{
  "$schema": "https://blode.co/ui/schema/registry-item.json",
  "name": "stepper",
  "title": "Stepper",
  "author": "Matthew Blode",
  "description": "A numbered progress indicator for a multi-step flow.",
  "dependencies": ["blode-icons-react", "class-variance-authority"],
  "files": [
    {
      "path": "ui/stepper.tsx",
      "content": "\"use client\";\n\nimport { CheckIcon } from \"blode-icons-react\";\nimport { cva } from \"class-variance-authority\";\nimport { Children, createContext, isValidElement, useContext } from \"react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype StepperOrientation = \"horizontal\" | \"vertical\";\n\n// Same vocabulary as `ProgressItemState` in progress-list.tsx, exposed on the same\n// `data-state` attribute, so a consumer styles both components with one selector.\ntype StepState = \"completed\" | \"current\" | \"pending\";\n\ninterface StepperContextValue {\n  count: number;\n  orientation: StepperOrientation;\n  value: number;\n}\n\nconst StepperContext = createContext<StepperContextValue | null>(null);\n\n// A step carries only its position. Everything else about it — completed or not, last or\n// not — follows from the root, so there is no second context object to keep in sync.\nconst StepIndexContext = createContext<number | null>(null);\n\nconst resolveState = (index: number, value: number): StepState => {\n  if (index < value) {\n    return \"completed\";\n  }\n  return index === value ? \"current\" : \"pending\";\n};\n\nconst useStepper = (): StepperContextValue => {\n  const context = useContext(StepperContext);\n  if (!context) {\n    throw new Error(\"Stepper parts must be used within a <Stepper>\");\n  }\n  return context;\n};\n\nconst useStep = () => {\n  const { count, value } = useStepper();\n  const index = useContext(StepIndexContext);\n\n  if (index === null) {\n    throw new Error(\"Step parts must be used within a <Step>\");\n  }\n\n  return { index, isLast: index === count - 1, state: resolveState(index, value) };\n};\n\nconst stateDescriptions: Record<StepState, string> = {\n  completed: \"completed\",\n  current: \"current step\",\n  pending: \"not started\",\n};\n\nexport interface StepperProps extends Omit<React.ComponentProps<\"ol\">, \"children\"> {\n  /** The steps of the flow — one `<Step>` per step */\n  children: React.ReactNode;\n  /** Layout direction (default: \"horizontal\") */\n  orientation?: StepperOrientation;\n  /** Zero-based index of the active step. Steps before it are completed */\n  value: number;\n}\n\nconst Stepper = ({\n  children,\n  className,\n  orientation = \"horizontal\",\n  value,\n  ...props\n}: StepperProps) => {\n  // oxlint-disable-next-line react/no-react-children -- index assignment only; children are never cloned or mutated\n  const items = Children.toArray(children);\n\n  return (\n    // oxlint-disable-next-line react/jsx-no-constructed-context-values -- the React Compiler memoises this; a manual useMemo here trips PreserveManualMemo instead\n    <StepperContext.Provider value={{ count: items.length, orientation, value }}>\n      <ol\n        className={cn(\n          \"flex w-full\",\n          orientation === \"horizontal\" ? \"items-start gap-4\" : \"flex-col\",\n          className,\n        )}\n        data-orientation={orientation}\n        data-slot=\"stepper\"\n        {...props}\n      >\n        {items.map((child, index) => (\n          <StepIndexContext.Provider\n            key={isValidElement(child) && child.key !== null ? child.key : index}\n            value={index}\n          >\n            {child}\n          </StepIndexContext.Provider>\n        ))}\n      </ol>\n    </StepperContext.Provider>\n  );\n};\n\nexport interface StepProps extends React.ComponentProps<\"li\"> {\n  /** Position of this step, when it is not a direct child of `<Stepper>` */\n  index?: number;\n}\n\nconst Step = ({ children, className, index: indexProp, ...props }: StepProps) => {\n  const { count, orientation, value } = useStepper();\n  const positionalIndex = useContext(StepIndexContext);\n  const index = indexProp ?? positionalIndex ?? 0;\n  const state = resolveState(index, value);\n\n  return (\n    <StepIndexContext.Provider value={index}>\n      <li\n        aria-current={state === \"current\" ? \"step\" : undefined}\n        className={cn(\n          // A bare <StepLabel> sits next to the 32px indicator, so it carries the 6px\n          // offset that optically centres its 20px line on the circle. Inside\n          // <StepContent> that offset is the container's padding instead.\n          \"group/step flex [&>[data-slot=step-label]]:mt-1.5\",\n          orientation === \"horizontal\"\n            ? \"min-w-0 flex-1 items-start gap-2 last:flex-none\"\n            : \"relative gap-3 pb-6 last:pb-0\",\n          className,\n        )}\n        data-slot=\"step\"\n        data-state={state}\n        {...props}\n      >\n        <span className=\"sr-only\">\n          Step {index + 1} of {count}, {stateDescriptions[state]}.\n        </span>\n        {children}\n      </li>\n    </StepIndexContext.Provider>\n  );\n};\n\nconst stepIndicatorVariants = cva(\n  \"flex size-8 shrink-0 items-center justify-center rounded-full border font-medium text-sm tabular-figures transition-[background-color,border-color,box-shadow,color] duration-150 ease-out [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0\",\n  {\n    defaultVariants: {\n      state: \"pending\",\n    },\n    variants: {\n      state: {\n        completed: \"border-primary bg-primary text-primary-foreground\",\n        current: \"border-primary bg-background text-foreground ring-2 ring-primary/25\",\n        pending: \"border-border bg-background text-muted-foreground\",\n      },\n    },\n  },\n);\n\nconst StepIndicator = ({ children, className, ...props }: React.ComponentProps<\"span\">) => {\n  const { index, state } = useStep();\n\n  let content: React.ReactNode = <CheckIcon aria-hidden=\"true\" />;\n  if (children) {\n    content = children;\n  } else if (state !== \"completed\") {\n    content = index + 1;\n  }\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(stepIndicatorVariants({ className, state }))}\n      data-slot=\"step-indicator\"\n      {...props}\n    >\n      {content}\n    </span>\n  );\n};\n\nconst StepContent = ({ className, ...props }: React.ComponentProps<\"span\">) => (\n  <span\n    className={cn(\"flex min-w-0 flex-col gap-0.5 pt-1.5\", className)}\n    data-slot=\"step-content\"\n    {...props}\n  />\n);\n\nconst stepLabelVariants = cva(\"font-medium text-sm\", {\n  defaultVariants: {\n    state: \"pending\",\n  },\n  variants: {\n    state: {\n      completed: \"text-foreground\",\n      current: \"text-foreground\",\n      pending: \"text-muted-foreground\",\n    },\n  },\n});\n\nconst StepLabel = ({ className, ...props }: React.ComponentProps<\"span\">) => {\n  const { state } = useStep();\n\n  return (\n    <span\n      className={cn(stepLabelVariants({ className, state }))}\n      data-slot=\"step-label\"\n      {...props}\n    />\n  );\n};\n\nconst StepDescription = ({ className, ...props }: React.ComponentProps<\"span\">) => (\n  <span\n    className={cn(\"text-muted-foreground text-xs\", className)}\n    data-slot=\"step-description\"\n    {...props}\n  />\n);\n\nconst StepSeparator = ({ className, ...props }: React.ComponentProps<\"span\">) => {\n  const { orientation } = useStepper();\n  const { isLast, state } = useStep();\n\n  if (isLast) {\n    return null;\n  }\n\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"bg-border transition-colors duration-150 ease-out\",\n        orientation === \"horizontal\"\n          ? \"mt-4 ml-2 h-px min-w-6 flex-1\"\n          : \"-translate-x-1/2 absolute top-9 bottom-1 left-4 w-px\",\n        state === \"completed\" && \"bg-primary\",\n        className,\n      )}\n      data-orientation={orientation}\n      data-slot=\"step-separator\"\n      data-state={state}\n      {...props}\n    />\n  );\n};\n\nexport { Step, StepContent, StepDescription, StepIndicator, StepLabel, StepSeparator, Stepper };\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}
