{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stepper",
  "type": "registry:ui",
  "title": "Stepper",
  "description": "Animated workflow steps with dotted upcoming connectors, solid completed connections, and glossy step controls.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/stepper.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, type KeyboardEvent } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface StepperProps {\n  steps: { id: string; title: string; description?: string }[];\n  currentStep: number;\n  onStepChange?: (step: number) => void;\n  disabled?: boolean;\n  orientation?: \"auto\" | \"horizontal\" | \"vertical\";\n  showDescriptions?: boolean;\n  animated?: boolean;\n  className?: string;\n  ariaLabel?: string;\n}\n\nexport const Stepper = forwardRef<HTMLElement, StepperProps>(function Stepper(\n  {\n    steps,\n    currentStep,\n    onStepChange,\n    disabled = false,\n    orientation = \"auto\",\n    showDescriptions = true,\n    animated = true,\n    className,\n    ariaLabel = \"Progress\",\n  },\n  ref,\n) {\n  const prefersReducedMotion = useReducedMotion();\n  const reduced = prefersReducedMotion || !animated;\n  const current = Number.isFinite(currentStep)\n    ? Math.max(0, Math.min(steps.length, Math.trunc(currentStep)))\n    : 0;\n  const transition = reduced\n    ? { duration: 0 }\n    : { type: \"spring\" as const, stiffness: 420, damping: 32 };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    if (event.altKey || event.ctrlKey || event.metaKey) return;\n    if (\n      ![\n        \"ArrowLeft\",\n        \"ArrowRight\",\n        \"ArrowUp\",\n        \"ArrowDown\",\n        \"Home\",\n        \"End\",\n      ].includes(event.key)\n    )\n      return;\n    const buttons = Array.from(\n      event.currentTarget\n        .closest(\"ol\")\n        ?.querySelectorAll<HTMLButtonElement>(\"button:not(:disabled)\") ?? [],\n    );\n    const index = buttons.indexOf(event.currentTarget);\n    const target =\n      event.key === \"Home\"\n        ? 0\n        : event.key === \"End\"\n          ? buttons.length - 1\n          : index + ([\"ArrowLeft\", \"ArrowUp\"].includes(event.key) ? -1 : 1);\n    event.preventDefault();\n    buttons[Math.max(0, Math.min(buttons.length - 1, target))]?.focus();\n  };\n\n  if (steps.length === 0) return null;\n\n  return (\n    <nav\n      ref={ref}\n      aria-label={ariaLabel}\n      data-orientation={orientation}\n      className={cn(\"@container group/stepper\", className)}\n    >\n      <ol className=\"flex flex-col gap-6 @md:group-data-[orientation=auto]/stepper:flex-row group-data-[orientation=horizontal]/stepper:flex-row @md:group-data-[orientation=auto]/stepper:gap-0 group-data-[orientation=horizontal]/stepper:gap-0\">\n        {steps.map((step, index) => {\n          const complete = index < current;\n          const active = index === current;\n          const interactive = Boolean(onStepChange);\n          const contents = (\n            <>\n              <motion.span\n                animate={{ scale: active && !reduced ? 1.06 : 1 }}\n                transition={transition}\n                className={cn(\n                  \"relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full border text-sm font-medium shadow-[0_4px_10px_-4px_rgba(0,0,0,0.25),inset_0_1px_3px_rgba(255,255,255,0.7),inset_0_-2px_4px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_12px_-4px_rgba(0,0,0,0.6),inset_0_1px_3px_rgba(255,255,255,0.2),inset_0_-2px_4px_rgba(0,0,0,0.2)]\",\n                  complete\n                    ? \"border-zinc-700 bg-[radial-gradient(120%_80%_at_50%_70%,#3f3f46_0%,#18181b_100%)] text-white dark:border-zinc-200 dark:bg-[radial-gradient(120%_80%_at_50%_70%,#ffffff_0%,#e4e4e7_100%)] dark:text-zinc-950\"\n                    : active\n                      ? \"border-zinc-400 bg-[radial-gradient(120%_80%_at_50%_70%,#ffffff_0%,#e4e4e7_100%)] text-zinc-950 ring-4 ring-zinc-900/5 dark:border-white/40 dark:bg-[radial-gradient(120%_80%_at_50%_70%,#3f3f46_0%,#18181b_100%)] dark:text-zinc-100 dark:ring-white/10\"\n                      : \"border-zinc-200 bg-zinc-100 text-zinc-500 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-500\",\n                )}\n                aria-hidden=\"true\"\n              >\n                <span className=\"pointer-events-none absolute inset-x-1 top-0 h-1/2 rounded-t-full bg-linear-to-b from-white/25 to-transparent\" />\n                <AnimatePresence mode=\"wait\" initial={false}>\n                  {complete ? (\n                    <motion.svg\n                      key=\"check\"\n                      viewBox=\"0 0 24 24\"\n                      className=\"h-5 w-5\"\n                      fill=\"none\"\n                      initial={{ opacity: reduced ? 1 : 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0 }}\n                      transition={transition}\n                    >\n                      <path\n                        d=\"m5 12 4 4 10-10\"\n                        stroke=\"currentColor\"\n                        strokeWidth=\"2\"\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                      />\n                    </motion.svg>\n                  ) : (\n                    <motion.span\n                      key=\"number\"\n                      initial={{ opacity: 0, y: reduced ? 0 : 6 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      exit={{ opacity: 0, y: reduced ? 0 : -6 }}\n                      transition={transition}\n                    >\n                      {index + 1}\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n              </motion.span>\n              <span className=\"min-w-0 text-left @md:group-data-[orientation=auto]/stepper:text-center group-data-[orientation=horizontal]/stepper:text-center\">\n                <span\n                  className={cn(\n                    \"block text-xs font-medium\",\n                    complete || active\n                      ? \"text-zinc-900 dark:text-zinc-100\"\n                      : \"text-zinc-500 dark:text-zinc-400\",\n                  )}\n                >\n                  {step.title}\n                </span>\n                {showDescriptions && step.description && (\n                  <span className=\"mt-1 block text-xs text-zinc-500 dark:text-zinc-400\">\n                    {step.description}\n                  </span>\n                )}\n                <span className=\"sr-only\">\n                  {complete\n                    ? \", completed\"\n                    : active\n                      ? \", current step\"\n                      : \", upcoming\"}\n                </span>\n              </span>\n            </>\n          );\n\n          return (\n            <li\n              key={step.id}\n              aria-current={active ? \"step\" : undefined}\n              className=\"relative min-w-0 @md:group-data-[orientation=auto]/stepper:flex-1 group-data-[orientation=horizontal]/stepper:flex-1\"\n            >\n              {index < steps.length - 1 && (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"absolute left-[calc(1.25rem-1.5px)] top-10 h-[calc(100%-1rem)] w-0.75 text-zinc-300 @md:group-data-[orientation=auto]/stepper:left-[calc(50%+1.5rem)] group-data-[orientation=horizontal]/stepper:left-[calc(50%+1.5rem)] @md:group-data-[orientation=auto]/stepper:top-[calc(1.25rem-1.5px)] group-data-[orientation=horizontal]/stepper:top-[calc(1.25rem-1.5px)] @md:group-data-[orientation=auto]/stepper:h-0.75 group-data-[orientation=horizontal]/stepper:h-0.75 @md:group-data-[orientation=auto]/stepper:w-[calc(100%-3rem)] group-data-[orientation=horizontal]/stepper:w-[calc(100%-3rem)] dark:text-zinc-700\"\n                >\n                  <motion.span\n                    animate={{ opacity: complete ? 0 : 1 }}\n                    initial={false}\n                    transition={transition}\n                    className=\"absolute inset-0 bg-[radial-gradient(circle,currentColor_1px,transparent_1.5px)] bg-size-[8px_8px] bg-center\"\n                  />\n                  <motion.span\n                    animate={{ opacity: complete ? 1 : 0 }}\n                    initial={false}\n                    transition={transition}\n                    className=\"absolute inset-px rounded-full bg-zinc-900 dark:bg-zinc-100\"\n                  />\n                </span>\n              )}\n              {interactive ? (\n                <button\n                  type=\"button\"\n                  disabled={disabled || index > current}\n                  onClick={() => onStepChange?.(index)}\n                  onKeyDown={handleKeyDown}\n                  className=\"relative flex w-full items-center gap-3 rounded-lg text-left outline-none focus-visible:ring-2 focus-visible:ring-zinc-500 focus-visible:ring-offset-4 focus-visible:ring-offset-white disabled:cursor-default @md:group-data-[orientation=auto]/stepper:flex-col group-data-[orientation=horizontal]/stepper:flex-col @md:group-data-[orientation=auto]/stepper:gap-3 group-data-[orientation=horizontal]/stepper:gap-3 dark:focus-visible:ring-offset-zinc-950\"\n                >\n                  {contents}\n                </button>\n              ) : (\n                <div className=\"relative flex items-center gap-3 @md:group-data-[orientation=auto]/stepper:flex-col group-data-[orientation=horizontal]/stepper:flex-col @md:group-data-[orientation=auto]/stepper:gap-3 group-data-[orientation=horizontal]/stepper:gap-3\">\n                  {contents}\n                </div>\n              )}\n            </li>\n          );\n        })}\n      </ol>\n    </nav>\n  );\n});\n\nStepper.displayName = \"Stepper\";\n",
      "type": "registry:ui",
      "target": "@ui/stepper.tsx"
    }
  ]
}
