{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","type":"registry:ui","title":"Stepper","description":"Shows where someone is in a multi-step flow and lets them go back.","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/stepper.tsx","type":"registry:ui","target":"components/ui/stepper.tsx","content":"\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport { type KeyboardEvent } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/chip-option.css\";\nimport \"./stepper.css\";\n\nexport type Step = {\n  id: string;\n  label: string;\n  description?: string;\n  error?: string;\n};\n\nexport function Stepper({\n  steps,\n  current,\n  orientation = \"horizontal\",\n  onStepSelect,\n  label = \"Progress\",\n  completeLabel = \"All steps complete\",\n  className,\n}: {\n  steps: readonly Step[];\n  current: number;\n  orientation?: \"horizontal\" | \"vertical\";\n  onStepSelect?: (index: number) => void;\n  label?: string;\n  completeLabel?: string;\n  className?: string;\n}) {\n  const active = Math.min(steps.length, Math.max(0, current));\n  const done = active >= steps.length;\n  const step = steps[Math.min(active, steps.length - 1)];\n  const announcement = !step ? \"\" : done ? completeLabel : `Step ${active + 1} of ${steps.length}: ${step.label}`;\n\n  function onKeyDown(event: KeyboardEvent<HTMLOListElement>) {\n    if (!onStepSelect) return;\n    const buttons = [...event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button:not([disabled])\")];\n    const index = buttons.indexOf(document.activeElement as HTMLButtonElement);\n    if (index < 0) return;\n    const next = event.key === \"ArrowRight\" || event.key === \"ArrowDown\" ? index + 1\n      : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\" ? index - 1\n      : event.key === \"Home\" ? 0\n      : event.key === \"End\" ? buttons.length - 1\n      : -1;\n    if (next < 0) return;\n    event.preventDefault();\n    buttons[Math.min(buttons.length - 1, Math.max(0, next))]?.focus();\n  }\n\n  return (\n    <nav aria-label={label} data-slot=\"stepper\" data-orientation={orientation} className={className}>\n      <p className=\"sr-only\" aria-live=\"polite\">{announcement}</p>\n      <ol className={cn(\"flex\", orientation === \"vertical\" ? \"flex-col gap-1\" : \"flex-col gap-1 sm:flex-row sm:items-start sm:gap-2\")} onKeyDown={onKeyDown}>\n        {steps.map((step, index) => {\n          const state = step.error ? \"error\" : index < active ? \"complete\" : index === active ? \"current\" : \"upcoming\";\n          const selectable = Boolean(onStepSelect) && state === \"complete\";\n          const body = (\n            <>\n              <span data-slot=\"stepper-marker\" data-state={state} className=\"grid size-6 shrink-0 place-items-center rounded-full border border-border text-xs font-medium\">\n                {state === \"complete\" ? <Check aria-hidden=\"true\" className=\"size-3.5\" /> : index + 1}\n              </span>\n              <span data-slot=\"stepper-copy\" className=\"min-w-0 text-left\">\n                <span className=\"block text-sm font-medium\">{step.label}</span>\n                {(step.error || step.description) && (\n                  <span className={cn(\"mt-0.5 block text-xs leading-[18px]\", step.error ? \"text-destructive\" : \"text-muted-foreground\")}>\n                    {step.error ?? step.description}\n                  </span>\n                )}\n                <span className=\"sr-only\">{state === \"complete\" ? \"Completed\" : state === \"error\" ? \"Error\" : state === \"current\" ? \"Current\" : \"Not started\"}</span>\n              </span>\n            </>\n          );\n          return (\n            <li key={step.id} data-state={state} className={cn(\"flex min-w-0 gap-2\", orientation === \"horizontal\" ? (index < steps.length - 1 ? \"flex-col sm:flex-1 sm:flex-row sm:items-start\" : \"flex-col sm:flex-row sm:items-start\") : \"flex-col\")}>\n              {selectable ? (\n                <button type=\"button\" className=\"flex min-w-0 gap-2 rounded-control text-left outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\" onClick={() => onStepSelect?.(index)}>\n                  {body}\n                </button>\n              ) : (\n                <div className=\"flex min-w-0 gap-2\" aria-current={state === \"current\" ? \"step\" : undefined} aria-disabled={state === \"upcoming\" || undefined}>\n                  {body}\n                </div>\n              )}\n              {index < steps.length - 1 && (\n                <span\n                  aria-hidden=\"true\"\n                  data-slot=\"stepper-line\"\n                  data-filled={index < active || undefined}\n                  className={cn(\n                    \"rounded-full bg-border transition-colors duration-[var(--duration-normal)] ease-out data-[filled]:bg-accent motion-reduce:transition-none\",\n                    orientation === \"horizontal\" ? \"ml-3 min-h-4 w-px sm:mt-3 sm:ml-0 sm:h-px sm:min-h-0 sm:w-auto sm:min-w-6 sm:flex-1\" : \"ml-3 min-h-4 w-px\",\n                  )}\n                />\n              )}\n            </li>\n          );\n        })}\n      </ol>\n      {done && <p className=\"mt-2 text-sm text-muted-foreground\">{completeLabel}</p>}\n    </nav>\n  );\n}\n"},{"path":"ui/shared/chip-option.css","type":"registry:file","target":"components/ui/shared/chip-option.css","content":"/* Shared by calendar.tsx, chart-parts.tsx, chip-group.tsx, data-table.tsx, number-field.tsx, password-field.tsx, stepper.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Depth sets 2 and 3 end */\n\n  /* Press feedback (2026-10-20, motion audit): every pressable control answers a press. Compact controls take the\n     system press scale, as Button does; wide rows deepen their fill instead, since scaling a full-width row reads as a\n     jump. Disabled controls stay still. */\n  :is([data-slot=\"chip-option\"], [data-slot=\"number-field\"] button, [data-slot=\"password-toggle\"], [data-slot=\"calendar\"] button, [data-slot=\"chart-legend-item\"], [data-slot=\"sort-button\"], [data-slot=\"stepper\"] button):active:not(:disabled):not([aria-disabled=\"true\"]) {\n    scale: var(--motion-press-scale);\n  }\n}\n"},{"path":"ui/stepper.css","type":"registry:file","target":"components/ui/stepper.css","content":"/* stepper.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"stepper-marker\"][data-state=\"complete\"],\n  [data-slot=\"stepper-marker\"][data-state=\"current\"] {\n    border-color: transparent;\n    background: var(--accent);\n    color: var(--accent-foreground);\n  }\n  [data-slot=\"stepper-marker\"][data-state=\"error\"] {\n    border-color: transparent;\n    background: var(--danger);\n    color: var(--danger-foreground);\n  }\n}\n"}],"categories":["feedback"],"meta":{"tier":"free","license":"MIT","family":"status","docs":"/docs/components/stepper"}}