{
  "slug": "stepper",
  "name": "Stepper",
  "description": "Animated workflow steps with dotted upcoming connectors, solid completed connections, and glossy step controls.",
  "summary": "A controlled monochrome stepper with spring-scaled indicators, fading completion checks, and dotted-to-solid connector crossfades. Container queries stack steps vertically in narrow containers and horizontally in larger ones. The parent owns progression, validation, and content. Completed steps can be revisited when onStepChange is provided.",
  "category": "feedback",
  "tags": [
    "stepper",
    "workflow",
    "progress",
    "onboarding",
    "motion"
  ],
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-02",
  "updatedDate": "2026-10-02",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Controlled progression and optional navigation to completed steps",
    "Transform and opacity animations with dotted-to-solid connectors and Candy-style highlights and shadows",
    "Responsive vertical and horizontal layouts",
    "Light and dark themes with reduced-motion support"
  ],
  "anatomy": [
    "Navigation landmark",
    "Ordered step list",
    "Number or completion check",
    "Animated connector and step label"
  ],
  "physics": {
    "engine": "Motion",
    "description": "Motion springs step indicators and fades completion checks and solid completed lines over dotted upcoming connectors.",
    "parameters": [
      {
        "label": "Spring",
        "value": "stiffness: 420, damping: 32"
      }
    ]
  },
  "accessibility": {
    "role": "navigation / list",
    "aria": "Current item exposes aria-current=step. Labels include completed, current, or upcoming status. Future step buttons are disabled.",
    "reducedMotion": "Transitions use zero duration, indicators do not scale or translate, and completion checks appear immediately.",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Move between enabled step buttons (buttons render only when onStepChange is provided)."
      },
      {
        "key": "Enter / Space",
        "description": "Activate the current or a completed step."
      },
      {
        "key": "Arrow keys",
        "description": "Move focus between enabled steps without changing the current step."
      },
      {
        "key": "Home / End",
        "description": "Focus the first or last enabled step."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Onboarding and short multi-step workflows"
    ],
    "bestPractices": [
      "Supply unique stable ids and descriptive step titles.",
      "Keep form validation and step content in the parent.",
      "Pass steps.length only once the workflow completes.",
      "Announce content changes and manage form focus in the parent.",
      "Use orientation=auto for small containers and long labels."
    ]
  },
  "props": [
    {
      "name": "steps",
      "type": "{ id: string; title: string; description?: string }[]",
      "description": "Ordered steps with stable unique ids.",
      "required": true
    },
    {
      "name": "currentStep",
      "type": "number",
      "description": "Zero-based current step. Set to steps.length to mark every step complete. Finite values are truncated and clamped; non-finite values use 0.",
      "required": true
    },
    {
      "name": "onStepChange",
      "type": "(step: number) => void",
      "description": "Enables buttons for the current and completed steps. Future steps remain disabled.",
      "required": false
    },
    {
      "name": "disabled",
      "type": "boolean",
      "description": "Disables all step buttons.",
      "required": false,
      "defaultValue": "false"
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional navigation container styles.",
      "required": false
    },
    {
      "name": "ariaLabel",
      "type": "string",
      "description": "Accessible label for the navigation landmark.",
      "required": false,
      "defaultValue": "Progress"
    },
    {
      "name": "orientation",
      "type": "\"auto\" | \"horizontal\" | \"vertical\"",
      "defaultValue": "auto",
      "description": "Auto stacks vertically in narrow containers and horizontally from 448px. Horizontal and vertical force the selected layout.",
      "required": false
    },
    {
      "name": "showDescriptions",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Show each step’s optional supporting description.",
      "required": false
    },
    {
      "name": "animated",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Enable Motion transitions. Reduced-motion preferences always take precedence.",
      "required": false
    },
    {
      "name": "ref",
      "type": "React.Ref<HTMLElement>",
      "description": "Ref forwarded to the navigation element for DOM access and measurement.",
      "required": false
    }
  ],
  "usage": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Stepper } from \"@/components/ui/stepper\";\n\nconst steps = [\n  { id: \"account\", title: \"Account\" },\n  { id: \"workspace\", title: \"Workspace\" },\n  { id: \"review\", title: \"Review\" },\n];\n\nexport function StepperDemo() {\n  const [step, setStep] = useState(1);\n\n  return <Stepper steps={steps} currentStep={step} onStepChange={setStep} />;\n}",
  "files": [
    {
      "name": "stepper.tsx",
      "path": "registry/ui/stepper.tsx",
      "code": "\"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",
      "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"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "Stepper"
}
