{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stepper",
  "type": "registry:ui",
  "title": "Stepper",
  "description": "Stepper shows progress through a fixed, known-length, ordered sequence of named steps — a compound Stepper + Step API built against the verified live Navigation/Step Item component set.",
  "author": "Skrewww",
  "dependencies": [
    "@phosphor-icons/react"
  ],
  "registryDependencies": [
    "@skrewww/foundation"
  ],
  "docs": "Host requirements (assumed already present, not installed by this command): react, react-dom.",
  "files": [
    {
      "path": "components/ui/Stepper.tsx",
      "content": "/** @skrewww-component stepper */\n\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { Check } from \"@phosphor-icons/react/dist/ssr\";\nimport { cn } from \"@/lib/cn\";\nimport styles from \"@/components/ui/stepper.module.css\";\n\nexport type StepStatus = \"completed\" | \"current\" | \"upcoming\";\n\nexport type StepperProps = {\n  /**\n   * Zero-based index of the current step, matching child order and the\n   * index passed to `onStepClick`. Stepper only reads this — it never\n   * mutates it; the app owns advancing/rewinding the sequence.\n   */\n  currentStep: number;\n  /**\n   * Fires with a step's index when a Completed or Current step is\n   * activated. Upcoming steps never call this — Stepper never lets users\n   * skip ahead. Omit to render a fully read-only progress indicator.\n   * Stepper does not navigate, mutate `currentStep`, or own routing; the\n   * app decides what happens on activation.\n   */\n  onStepClick?: (index: number) => void;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  className?: string;\n  children: ReactNode;\n};\n\n/** Injected by Stepper onto each Step child — not part of Step's public API. */\ntype StepInjectedProps = {\n  index: number;\n  status: StepStatus;\n  interactive: boolean;\n  onActivate?: (index: number) => void;\n  isLast: boolean;\n};\n\n/**\n * Fixed, known-length, horizontal sequence of named steps with\n * Completed/Current/Upcoming state, derived structurally from child order\n * and `currentStep` — never a manually-assigned per-step state prop.\n *\n * Built against the verified live Figma source \"Navigation/Step Item\"\n * (node 2024:2944, 3 variants) and its composed \"Stepper Trail (example)\"\n * reference frame (node 2024:2949) — see lib/stepper-figma-metadata.ts.\n * There is no separate Figma \"Stepper\" component; only Step Item is real.\n *\n * Not Progress Bar (quantitative percentage), not Tabs (peer content\n * views), not Breadcrumb (hierarchy), not Pagination (page navigation),\n * not Timeline (open-ended chronological history).\n */\nexport function Stepper({\n  currentStep,\n  onStepClick,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  className,\n  children,\n}: StepperProps) {\n  const items = Children.toArray(children).filter(isValidElement) as ReactElement<StepProps>[];\n  const total = items.length;\n\n  return (\n    <ol\n      aria-label={ariaLabel}\n      aria-labelledby={ariaLabelledBy}\n      className={cn(styles.stepper, className)}\n    >\n      {items.map((child, index) => {\n        const status: StepStatus =\n          index < currentStep ? \"completed\" : index === currentStep ? \"current\" : \"upcoming\";\n        const interactive = Boolean(onStepClick) && status !== \"upcoming\";\n        const injected: StepInjectedProps = {\n          index,\n          status,\n          interactive,\n          onActivate: onStepClick,\n          isLast: index === total - 1,\n        };\n        return cloneElement(child, { key: child.key ?? index, ...injected });\n      })}\n    </ol>\n  );\n}\n\nexport type StepProps = {\n  /** The step's label. Skrewww renders only this — no description/subtitle/metadata/badge/icon slot in v1. */\n  children: ReactNode;\n  className?: string;\n};\n\nconst statusClass: Record<StepStatus, string> = {\n  completed: styles.circleCompleted,\n  current: styles.circleCurrent,\n  upcoming: styles.circleUpcoming,\n};\n\nconst labelStatusClass: Record<StepStatus, string> = {\n  completed: styles.labelCompleted,\n  current: styles.labelCurrent,\n  upcoming: styles.labelUpcoming,\n};\n\n/**\n * A single step within a Stepper. `status`/`index` are supplied by Stepper\n * at render time — Step never accepts them as public props, matching the\n * verified Figma contract (no `state` property surfaced on the item\n * itself; state is structural).\n */\nexport function Step({\n  children,\n  className,\n  index,\n  status,\n  interactive,\n  onActivate,\n  isLast,\n}: StepProps & Partial<StepInjectedProps>) {\n  if (status === undefined || index === undefined) {\n    throw new Error(\"Step must be used within Stepper\");\n  }\n  const stepIndex = index;\n\n  const indicator = (\n    <span className={cn(styles.circle, statusClass[status])} aria-hidden=\"true\">\n      {status === \"completed\" ? (\n        <Check weight=\"bold\" className={styles.checkIcon} />\n      ) : (\n        <span className={styles.number}>{index + 1}</span>\n      )}\n    </span>\n  );\n\n  const label = <span className={cn(styles.label, labelStatusClass[status])}>{children}</span>;\n\n  function handleClick() {\n    if (!interactive) return;\n    onActivate?.(stepIndex);\n  }\n\n  return (\n    <li className={cn(styles.item, className)}>\n      {interactive ? (\n        <button\n          type=\"button\"\n          className={styles.control}\n          aria-current={status === \"current\" ? \"step\" : undefined}\n          onClick={handleClick}\n        >\n          {indicator}\n          {label}\n        </button>\n      ) : (\n        <span className={styles.control} aria-current={status === \"current\" ? \"step\" : undefined}>\n          {indicator}\n          {label}\n        </span>\n      )}\n      {!isLast ? <span className={styles.connector} aria-hidden=\"true\" /> : null}\n    </li>\n  );\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/Stepper.tsx"
    },
    {
      "path": "components/ui/stepper.module.css",
      "content": "/*\n * Verified live against Figma \"Navigation/Step Item\" (node 2024:2944) and\n * the composed \"Stepper Trail (example)\" frame (node 2024:2949) — see\n * lib/stepper-figma-metadata.ts. The same 8px gap governs circle-to-label,\n * step-to-connector, and connector-to-step spacing throughout — confirmed,\n * not coincidental.\n */\n\n.stepper {\n  box-sizing: border-box;\n  display: flex;\n  align-items: center;\n  gap: 0.5rem; /* 8px — verified itemSpacing */\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.item {\n  box-sizing: border-box;\n  display: inline-flex;\n  align-items: center;\n  gap: 0.5rem; /* 8px — verified itemSpacing */\n}\n\n.control {\n  box-sizing: border-box;\n  display: inline-flex;\n  align-items: center;\n  gap: 0.5rem; /* 8px — verified circle-to-label gap */\n  border: none;\n  background: none;\n  padding: 0;\n  margin: 0;\n  font-family: inherit;\n  cursor: default;\n  text-align: left;\n}\n\nbutton.control {\n  cursor: pointer;\n}\n\nbutton.control:focus-visible {\n  outline: 2px solid var(--semantic-focus-ring);\n  outline-offset: 2px;\n  border-radius: var(--radius-full, 9999px);\n}\n\n.circle {\n  box-sizing: border-box;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  width: 1.5rem; /* 24px — verified */\n  height: 1.5rem; /* 24px — verified */\n  border-radius: var(--radius-full, 9999px); /* [VERIFIED] primitive radius/full — no Shape-system participation */\n  border-style: solid;\n  border-color: transparent;\n}\n\n.circleCompleted {\n  background-color: var(--semantic-action-primary);\n}\n\n.circleCurrent {\n  background-color: var(--semantic-surface-default);\n  border-width: 1.5px;\n  border-color: var(--semantic-action-primary);\n}\n\n.circleUpcoming {\n  background-color: var(--semantic-surface-default);\n  border-width: 1.5px;\n  border-color: var(--semantic-border-default);\n}\n\n.checkIcon {\n  width: 0.875rem;\n  height: 0.875rem;\n  color: var(--semantic-text-inverse);\n}\n\n.number {\n  font-size: 0.8125rem;\n  line-height: 1;\n  color: inherit;\n}\n\n.circleCurrent .number {\n  color: var(--semantic-text-primary);\n}\n\n.circleUpcoming .number {\n  color: var(--component-surface-content-muted);\n}\n\n.label {\n  font-size: 1rem; /* 16px — verified */\n  line-height: 1.2;\n  white-space: nowrap;\n}\n\n.labelCompleted {\n  color: var(--semantic-text-primary);\n  font-weight: 400; /* [VERIFIED] */\n}\n\n.labelCurrent {\n  color: var(--semantic-text-primary);\n  font-weight: 700; /* [VERIFIED] — a non-color signal distinguishing Current */\n}\n\n.labelUpcoming {\n  color: var(--component-surface-content-muted);\n  font-weight: 400; /* [VERIFIED] */\n}\n\n.connector {\n  flex-shrink: 0;\n  width: 2rem; /* 32px — verified */\n  height: 1.5px; /* verified */\n  background-color: var(--semantic-border-default);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .control {\n    transition: none;\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/stepper.module.css"
    },
    {
      "path": "lib/cn.ts",
      "content": "export function cn(...classes: Array<string | false | null | undefined>): string {\n  return classes.filter(Boolean).join(\" \");\n}\n",
      "type": "registry:lib",
      "target": "~/lib/cn.ts"
    }
  ]
}
