{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "step-indicator",
  "title": "Step Indicator",
  "description": "Horizontal stepper that shows where someone is in a fixed sequence — numbered circle markers joined by a connecting line, each drawn as complete (filled, with a check), current (ringed and highlighted) or upcoming (muted). Pass the steps and a current index and it derives every state; there is nothing to keep in sync by hand. Reach for it at the top of anything multi-step: a checkout or cart flow, a signup and onboarding wizard, account or workspace setup, a KYC or identity-verification flow, a document or tax filing, a multi-page form split across screens, an upload-then-review-then-publish pipeline, a survey or quiz, or an installer. Common asks it answers: \"stepper component\", \"step indicator react\", \"multi-step form progress\", \"wizard steps ui\", \"checkout progress bar with steps\", \"onboarding progress indicator\", \"shadcn stepper\", \"progress steps 1 2 3\", \"form wizard header\". shadcn/ui has no stepper: its progress component is a single bar with no notion of discrete stages, labels or a current position, and while its questionnaire is a multi-step flow, that component owns the questions and answers and reports its place as a plain \"3 of 8\" counter rather than a rail of numbered markers — so the header that shows where someone is in a sequence you already control still gets rebuilt by hand out of divs and borders. It is also not a timeline: this one counts position through a sequence that is known in advance and still to be finished, while timeline is the record of what already happened and has no current step. Built as an ordered list, because the steps are an ordered list: the active one carries aria-current=\"step\", every marker states its own status in screen-reader-only text (Completed / Current step / Not completed) rather than leaving the meaning to a colour and a tick, and the check icon is aria-hidden so it is not announced twice. Conveying stage by colour alone fails WCAG 1.4.1, which is why the status is always spelled out. Pass onStepClick and the steps already reached become real buttons with a focus-visible ring, while upcoming steps stay inert — a stepper that lets someone jump forward past validation is worse than one that is not clickable at all. Theme-aware through shadcn tokens with dark mode, and the only dependency is lucide-react for the check icon.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/step-indicator.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface Step {\n  /** Short label shown beneath the step marker. */\n  label: string\n  /** Optional secondary line, e.g. a hint or status. */\n  description?: string\n}\n\ntype StepStatus = \"complete\" | \"current\" | \"upcoming\"\n\ninterface StepIndicatorProps\n  extends Omit<React.ComponentPropsWithoutRef<\"ol\">, \"onClick\"> {\n  /** Ordered steps, rendered left to right. */\n  steps: Step[]\n  /**\n   * Zero-based index of the active step. Everything before it is treated as\n   * complete, everything after it as upcoming.\n   */\n  current: number\n  /**\n   * Make already-reached steps (complete + current) navigable. Fires with the\n   * step index. Upcoming steps stay non-interactive. Omit for a display-only\n   * indicator.\n   */\n  onStepClick?: (index: number) => void\n  /** Accessible name for the progress list, e.g. \"Checkout\" or \"Onboarding\". */\n  \"aria-label\"?: string\n}\n\nconst STATUS_LABEL: Record<StepStatus, string> = {\n  complete: \"Completed\",\n  current: \"Current step\",\n  upcoming: \"Not completed\",\n}\n\nexport const StepIndicator = React.forwardRef<\n  HTMLOListElement,\n  StepIndicatorProps\n>(function StepIndicator(\n  { className, steps, current, onStepClick, \"aria-label\": ariaLabel, ...props },\n  ref\n) {\n  const lastIndex = steps.length - 1\n\n  return (\n    <ol\n      ref={ref}\n      aria-label={ariaLabel}\n      className={cn(\"flex w-full\", className)}\n      {...props}\n    >\n      {steps.map((step, i) => {\n        const status: StepStatus =\n          i < current ? \"complete\" : i === current ? \"current\" : \"upcoming\"\n        const isLast = i === lastIndex\n        // Reached steps can be revisited; upcoming ones can't.\n        const clickable = Boolean(onStepClick) && status !== \"upcoming\"\n\n        const marker = (\n          <span\n            className={cn(\n              \"relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-2 text-sm font-medium transition-colors\",\n              status === \"complete\" &&\n                \"border-transparent bg-primary text-primary-foreground\",\n              status === \"current\" &&\n                \"border-primary bg-background text-primary\",\n              status === \"upcoming\" &&\n                \"border-muted-foreground/30 bg-background text-muted-foreground\",\n              clickable &&\n                \"transition-transform group-hover:scale-105 group-focus-visible:scale-105\"\n            )}\n          >\n            {status === \"complete\" ? (\n              <Check className=\"h-4 w-4\" aria-hidden=\"true\" />\n            ) : (\n              i + 1\n            )}\n          </span>\n        )\n\n        const labelBlock = (\n          <span className=\"mt-2 flex max-w-[9rem] flex-col items-center gap-0.5 px-1 text-center\">\n            <span\n              className={cn(\n                \"text-sm font-medium leading-tight\",\n                status === \"upcoming\"\n                  ? \"text-muted-foreground\"\n                  : \"text-foreground\"\n              )}\n            >\n              {step.label}\n            </span>\n            {step.description ? (\n              <span className=\"text-xs leading-tight text-muted-foreground\">\n                {step.description}\n              </span>\n            ) : null}\n          </span>\n        )\n\n        return (\n          <li\n            key={i}\n            aria-current={status === \"current\" ? \"step\" : undefined}\n            className=\"relative flex flex-1 flex-col items-center\"\n          >\n            {/* Connector to the next marker: spans from this marker's centre\n                one full (equal-width) cell to the right, i.e. the next centre.\n                Markers sit above it (z-10, solid bg) so the ends are masked. */}\n            {!isLast ? (\n              <span\n                aria-hidden=\"true\"\n                className={cn(\n                  \"absolute left-1/2 top-4 h-0.5 w-full -translate-y-1/2\",\n                  i < current ? \"bg-primary\" : \"bg-border\"\n                )}\n              />\n            ) : null}\n\n            {clickable ? (\n              <button\n                type=\"button\"\n                onClick={() => onStepClick?.(i)}\n                aria-label={`Go to step ${i + 1}: ${step.label}`}\n                className=\"group flex flex-col items-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n              >\n                {marker}\n                {labelBlock}\n              </button>\n            ) : (\n              <div className=\"flex flex-col items-center\">\n                {marker}\n                {labelBlock}\n              </div>\n            )}\n\n            <span className=\"sr-only\">{STATUS_LABEL[status]}</span>\n          </li>\n        )\n      })}\n    </ol>\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}