{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkout-stepper",
  "title": "Checkout Stepper",
  "author": "ecomcn <hello@ecomcn.dev>",
  "description": "Where the shopper is in checkout: a rail of equal ruled cells, edge to edge. Done steps carry a check and are buttons back to them; the current step is aria-current=\"step\"; steps to come are not clickable. Never colour alone — a check, a number, and a heavy rule under the current step that slides on as the shopper moves (Motion; a fade under reduced motion). On a phone the rail shows numbers, with the step named below it.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "src/registry/ecomcn/checkout-stepper/checkout-stepper.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check } from \"lucide-react\"\nimport { MotionConfig, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/*\n * Where the shopper is in checkout: a rail of equal ruled cells, edge to\n * edge — not floating pills.\n *\n *   <CheckoutStepper steps={STEPS} value={step} onValueChange={setStep} />\n *\n * Done steps carry a check and are buttons that go back to them; the\n * current one is marked `aria-current=\"step\"`; steps still to come are not\n * clickable — you can't skip ahead of what checkout needs. State is never\n * colour alone: a check, a number, and the heavy rule under the current\n * step, which slides to the next as the shopper moves on (Motion; a fade\n * under reduced motion). On a phone the labels give way to numbers, with\n * the current step named in full below the rail.\n */\n\nexport interface CheckoutStep {\n  id: string\n  label: string\n}\n\n/** The same settle curve as the other ecomcn morphs: no bounce, an editorial ease. */\nconst MORPH = { duration: 0.45, ease: [0.2, 0, 0, 1] } as const\n\nexport interface CheckoutStepperProps extends Omit<React.HTMLAttributes<HTMLElement>, \"onChange\"> {\n  steps: CheckoutStep[]\n  /** The current step's id. */\n  value: string\n  /** Called with a done step's id when the shopper goes back to it. */\n  onValueChange?: (id: string) => void\n  /** \"Checkout progress\", by default. */\n  label?: string\n}\n\nexport function CheckoutStepper({\n  steps,\n  value,\n  onValueChange,\n  label = \"Checkout progress\",\n  className,\n  ...props\n}: CheckoutStepperProps) {\n  const reduce = useReducedMotion()\n  const scope = React.useId()\n  const current = Math.max(0, steps.findIndex((s) => s.id === value))\n  const step = steps[current]\n\n  if (!steps.length) return null\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <nav aria-label={label} data-slot=\"checkout-stepper\" className={cn(\"w-full\", className)} {...props}>\n        <ol\n          className=\"grid border-y\"\n          style={{ gridTemplateColumns: `repeat(${steps.length}, minmax(0, 1fr))` }}\n        >\n          {steps.map((s, i) => {\n            const state = i < current ? \"done\" : i === current ? \"current\" : \"pending\"\n            const cell = cn(\n              \"relative flex h-12 w-full items-center gap-2.5 px-3 text-left text-sm outline-none sm:px-4\",\n              i > 0 && \"border-l\"\n            )\n            const marker = (\n              <span\n                aria-hidden\n                className={cn(\n                  \"grid size-5 shrink-0 place-items-center text-[11px] tabular-nums\",\n                  state === \"done\" && \"bg-foreground text-background\",\n                  state === \"current\" && \"border border-foreground\",\n                  state === \"pending\" && \"border border-border text-muted-foreground\"\n                )}\n              >\n                {state === \"done\" ? <Check className=\"size-3\" /> : i + 1}\n              </span>\n            )\n            const text = (\n              <span\n                className={cn(\n                  \"hidden truncate sm:inline\",\n                  state === \"pending\" ? \"text-muted-foreground\" : \"text-foreground\",\n                  state === \"current\" && \"font-medium\"\n                )}\n              >\n                {s.label}\n              </span>\n            )\n            return (\n              <li key={s.id} data-state={state} className=\"relative min-w-0\">\n                {state === \"done\" && onValueChange ? (\n                  <button\n                    type=\"button\"\n                    onClick={() => onValueChange(s.id)}\n                    className={cn(cell, \"transition-colors hover:bg-secondary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\")}\n                  >\n                    {marker}\n                    {text}\n                    <span className=\"sr-only\">{`${s.label}, done. Go back to this step`}</span>\n                  </button>\n                ) : (\n                  <span aria-current={state === \"current\" ? \"step\" : undefined} className={cell}>\n                    {marker}\n                    {text}\n                    <span className=\"sr-only\">\n                      {state === \"current\" ? `${s.label}, current step` : state === \"done\" ? `${s.label}, done` : `${s.label}, not started`}\n                    </span>\n                  </span>\n                )}\n                {state === \"current\" ? (\n                  <motion.span\n                    aria-hidden\n                    layoutId={reduce ? undefined : `${scope}-current`}\n                    initial={reduce ? { opacity: 0 } : false}\n                    animate={{ opacity: 1 }}\n                    transition={reduce ? { duration: 0.15 } : MORPH}\n                    className=\"absolute inset-x-0 -bottom-px h-0.5 bg-foreground\"\n                  />\n                ) : null}\n              </li>\n            )\n          })}\n        </ol>\n        {/* A phone shows numbers in the rail; the step is named here. */}\n        <p className=\"mt-2 text-sm sm:hidden\">\n          <span className=\"text-muted-foreground tabular-nums\">\n            Step {current + 1} of {steps.length}:\n          </span>{\" \"}\n          {step?.label}\n        </p>\n      </nav>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ecomcn/checkout-stepper.tsx"
    }
  ],
  "categories": [
    "ecommerce",
    "checkout"
  ],
  "type": "registry:component"
}