{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stepper",
  "title": "UI Stepper",
  "description": "A horizontal onboarding stepper whose progress is a pure function of a continuous step index (value-channel deviation). Every circle (fill, number, stroke-dash check) and connector (primary fill fraction) derives purely from the float position; useStepperTransition reads the frame and eases position between step indices.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/remocn-ui"],
  "files": [
    {
      "path": "registry/remocn-ui/stepper/index.tsx",
      "content": "\"use client\";\n\nimport {\n  clamp01,\n  mixOklch,\n  type RemocnTheme,\n  useRemocnTheme,\n} from \"@/lib/remocn-ui\";\n\nexport interface StepperStyle {\n  position: number;\n}\n\nexport type StepperOrientation = \"horizontal\" | \"vertical\";\n\nexport interface StepperProps {\n  steps?: string[];\n  activeIndex?: number;\n  style?: StepperStyle;\n  orientation?: StepperOrientation;\n  theme?: Partial<RemocnTheme>;\n  className?: string;\n}\n\nconst DEFAULT_STEPS = [\"Account\", \"Plan\", \"Done\"];\n\nconst CIRCLE = 36;\nconst CHECK_PATH_LENGTH = 14;\n\nexport interface StepperStyleContext {\n  primary: string;\n  primaryFg: string;\n  mutedBg: string;\n  border: string;\n  mutedFg: string;\n  foreground: string;\n}\n\nexport function stepperStyleContext(theme: RemocnTheme): StepperStyleContext {\n  return {\n    primary: theme.primary,\n    primaryFg: theme.primaryForeground,\n    mutedBg: theme.muted,\n    border: theme.border,\n    mutedFg: theme.mutedForeground,\n    foreground: theme.foreground,\n  };\n}\n\nexport function stepperStyle(activeIndex: number): StepperStyle {\n  return { position: activeIndex };\n}\n\nexport function stepCircleAt(\n  i: number,\n  position: number,\n): {\n  fill: number;\n  checkDraw: number;\n  active: boolean;\n} {\n  const fill = clamp01(position - i);\n  const checkDraw = fill;\n  const active = Math.floor(position) === i && fill < 1;\n  return { fill, checkDraw, active };\n}\n\nexport function connectorFillAt(i: number, position: number): number {\n  return clamp01(position - i);\n}\n\nexport function Stepper({\n  steps = DEFAULT_STEPS,\n  activeIndex = 0,\n  style,\n  orientation: _orientation = \"horizontal\",\n  theme: themeOverride,\n  className,\n}: StepperProps) {\n  const theme = useRemocnTheme(themeOverride, \"light\");\n  const ctx = stepperStyleContext(theme);\n  const v = style ?? stepperStyle(activeIndex);\n  const position = v.position;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        background: \"transparent\",\n        fontFamily:\n          \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\",\n      }}\n    >\n      <div style={{ display: \"flex\", alignItems: \"flex-start\" }}>\n        {steps.map((label, i) => {\n          const { fill, checkDraw, active } = stepCircleAt(i, position);\n          const completed = fill >= 1;\n          const circleBg = mixOklch(ctx.mutedBg, ctx.primary, fill);\n          const circleBorder = mixOklch(ctx.border, ctx.primary, fill);\n          const numberOpacity = 1 - checkDraw;\n          const numberColor =\n            active || completed ? ctx.foreground : ctx.mutedFg;\n          const isLast = i === steps.length - 1;\n          return (\n            <div\n              key={label}\n              style={{ display: \"flex\", alignItems: \"flex-start\" }}\n            >\n              {}\n              <div\n                style={{\n                  display: \"flex\",\n                  flexDirection: \"column\",\n                  alignItems: \"center\",\n                  gap: 8,\n                  width: CIRCLE + 24,\n                }}\n              >\n                <div\n                  style={{\n                    position: \"relative\",\n                    width: CIRCLE,\n                    height: CIRCLE,\n                    borderRadius: \"50%\",\n                    background: circleBg,\n                    border: `2px solid ${active ? ctx.primary : circleBorder}`,\n                    boxSizing: \"border-box\",\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    justifyContent: \"center\",\n                  }}\n                >\n                  {}\n                  <span\n                    style={{\n                      position: \"absolute\",\n                      fontSize: 14,\n                      fontWeight: 600,\n                      color: numberColor,\n                      opacity: numberOpacity,\n                    }}\n                  >\n                    {i + 1}\n                  </span>\n                  {}\n                  <svg\n                    width={CIRCLE}\n                    height={CIRCLE}\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                  >\n                    <path\n                      d=\"M5 12.5l4.5 4.5L19 7\"\n                      stroke={ctx.primaryFg}\n                      strokeWidth=\"2.6\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      pathLength={CHECK_PATH_LENGTH}\n                      strokeDasharray={CHECK_PATH_LENGTH}\n                      strokeDashoffset={CHECK_PATH_LENGTH * (1 - checkDraw)}\n                    />\n                  </svg>\n                </div>\n                <span\n                  style={{\n                    fontSize: 13,\n                    fontWeight: 500,\n                    letterSpacing: \"-0.01em\",\n                    color: active || completed ? ctx.foreground : ctx.mutedFg,\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  {label}\n                </span>\n              </div>\n              {}\n              {!isLast && (\n                <div\n                  style={{\n                    position: \"relative\",\n                    width: 64,\n                    height: 2,\n                    marginTop: CIRCLE / 2 - 1,\n                    background: ctx.border,\n                    overflow: \"hidden\",\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      left: 0,\n                      top: 0,\n                      bottom: 0,\n                      width: `${connectorFillAt(i, position) * 100}%`,\n                      background: ctx.primary,\n                    }}\n                  />\n                </div>\n              )}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/stepper.tsx"
    },
    {
      "path": "registry/remocn-ui/stepper/use-stepper-transition.ts",
      "content": "\"use client\";\n\nimport { useCurrentFrame } from \"remotion\";\nimport type { StepperStyle } from \"@/components/remocn/stepper\";\nimport { clamp01, type EasingName, easings } from \"@/lib/remocn-ui\";\n\nexport interface StepperStep {\n  at: number;\n  index: number;\n  duration?: number;\n  easing?: EasingName;\n}\n\nexport const DEFAULT_DURATION = 24;\n\nexport interface StepperTransitionOptions {\n  speed?: number;\n  defaultDuration?: number;\n}\n\nexport function tweenStepperStyle(\n  a: StepperStyle,\n  b: StepperStyle,\n  t: number,\n): StepperStyle {\n  return { position: a.position + (b.position - a.position) * t };\n}\n\nexport function useStepperTransition(\n  steps: StepperStep[],\n  opts: StepperTransitionOptions = {},\n): StepperStyle {\n  const { speed = 1 } = opts;\n  const raw = useCurrentFrame() * speed;\n  return stepperStyleAt(steps, raw, opts);\n}\n\nexport function stepperStyleAt(\n  steps: StepperStep[],\n  raw: number,\n  opts: StepperTransitionOptions = {},\n): StepperStyle {\n  const { defaultDuration = DEFAULT_DURATION } = opts;\n\n  if (steps.length === 0) return { position: 0 };\n\n  const first = steps[0];\n\n  if (raw <= first.at) return { position: first.index };\n\n  let toIndex = steps.length - 1;\n  for (let i = 1; i < steps.length; i++) {\n    if (steps[i].at > raw) {\n      toIndex = i;\n      break;\n    }\n  }\n  const pastLast = raw >= steps[steps.length - 1].at;\n  const to = pastLast ? steps[steps.length - 1] : steps[toIndex];\n  const from = pastLast ? steps[steps.length - 1] : steps[toIndex - 1];\n\n  const dur = to.duration ?? defaultDuration;\n  const ease = easings[to.easing ?? \"out\"];\n  const start = to.at - dur;\n  const t = pastLast || dur <= 0 ? 1 : ease(clamp01((raw - start) / dur));\n  const position = from.index + (to.index - from.index) * t;\n\n  return { position };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/use-stepper-transition.ts"
    }
  ],
  "type": "registry:component"
}
