{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","type":"registry:ui","title":"Stepper","description":"","dependencies":["radix-ui"],"registryDependencies":[],"files":[{"path":"stepper.tsx","type":"registry:ui","content":"/* eslint-disable react-hooks/exhaustive-deps */\n\n\"use client\"\n\nimport type { HTMLAttributes, ReactElement } from \"react\"\nimport {\n Children,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\n\n// Types\ntype StepperOrientation = \"horizontal\" | \"vertical\"\ntype StepState = \"active\" | \"completed\" | \"inactive\" | \"loading\"\ntype StepIndicators = {\n active?: React.ReactNode\n completed?: React.ReactNode\n inactive?: React.ReactNode\n loading?: React.ReactNode\n}\n\ninterface StepperContextValue {\n activeStep: number\n setActiveStep: (step: number) => void\n stepsCount: number\n orientation: StepperOrientation\n registerTrigger: (node: HTMLButtonElement | null) => void\n triggerNodes: HTMLButtonElement[]\n focusNext: (currentIdx: number) => void\n focusPrev: (currentIdx: number) => void\n focusFirst: () => void\n focusLast: () => void\n indicators: StepIndicators\n}\n\ninterface StepItemContextValue {\n step: number\n state: StepState\n isDisabled: boolean\n isLoading: boolean\n}\n\nconst StepperContext = createContext(undefined)\nconst StepItemContext = createContext(\n undefined\n)\n\nfunction useStepper() {\n const ctx = useContext(StepperContext)\n if (!ctx) throw new Error(\"useStepper must be used within a Stepper\")\n return ctx\n}\n\nfunction useStepItem() {\n const ctx = useContext(StepItemContext)\n if (!ctx) throw new Error(\"useStepItem must be used within a StepperItem\")\n return ctx\n}\n\ninterface StepperProps extends HTMLAttributes {\n defaultValue?: number\n value?: number\n onValueChange?: (value: number) => void\n orientation?: StepperOrientation\n indicators?: StepIndicators\n}\n\nfunction Stepper({\n defaultValue = 1,\n value,\n onValueChange,\n orientation = \"horizontal\",\n className,\n children,\n indicators = {},\n ...props\n}: StepperProps) {\n const [activeStep, setActiveStep] = useState(defaultValue)\n const [triggerNodes, setTriggerNodes] = useState([])\n\n // Register/unregister triggers\n const registerTrigger = useCallback((node: HTMLButtonElement | null) => {\n setTriggerNodes((prev) => {\n if (node && !prev.includes(node)) {\n return [...prev, node]\n } else if (!node && prev.includes(node!)) {\n return prev.filter((n) => n !== node)\n } else {\n return prev\n }\n })\n }, [])\n\n const handleSetActiveStep = useCallback(\n (step: number) => {\n if (value === undefined) {\n setActiveStep(step)\n }\n onValueChange?.(step)\n },\n [value, onValueChange]\n )\n\n const currentStep = value ?? activeStep\n\n // Keyboard navigation logic\n const focusTrigger = (idx: number) => {\n if (triggerNodes[idx]) triggerNodes[idx].focus()\n }\n const focusNext = (currentIdx: number) =>\n focusTrigger((currentIdx + 1) % triggerNodes.length)\n const focusPrev = (currentIdx: number) =>\n focusTrigger((currentIdx - 1 + triggerNodes.length) % triggerNodes.length)\n const focusFirst = () => focusTrigger(0)\n const focusLast = () => focusTrigger(triggerNodes.length - 1)\n\n // Context value\n const contextValue = useMemo(\n () => ({\n activeStep: currentStep,\n setActiveStep: handleSetActiveStep,\n stepsCount: Children.toArray(children).filter(\n (child): child is ReactElement =>\n isValidElement(child) &&\n (child.type as { displayName?: string }).displayName === \"StepperItem\"\n ).length,\n orientation,\n registerTrigger,\n focusNext,\n focusPrev,\n focusFirst,\n focusLast,\n triggerNodes,\n indicators,\n }),\n [\n currentStep,\n handleSetActiveStep,\n children,\n orientation,\n registerTrigger,\n triggerNodes,\n ]\n )\n\n return (\n \n \n {children}\n \n \n )\n}\n\ninterface StepperItemProps extends React.HTMLAttributes {\n step: number\n completed?: boolean\n disabled?: boolean\n loading?: boolean\n}\n\nfunction StepperItem({\n step,\n completed = false,\n disabled = false,\n loading = false,\n className,\n children,\n ...props\n}: StepperItemProps) {\n const { activeStep } = useStepper()\n\n const state: StepState =\n completed || step < activeStep\n ? \"completed\"\n : activeStep === step\n ? \"active\"\n : \"inactive\"\n\n const isLoading = loading && step === activeStep\n\n return (\n \n \n {children}\n \n \n )\n}\n\ninterface StepperTriggerProps extends React.ButtonHTMLAttributes {\n asChild?: boolean\n}\n\nfunction StepperTrigger({\n asChild = false,\n className,\n children,\n tabIndex,\n ...props\n}: StepperTriggerProps) {\n const { state, isLoading } = useStepItem()\n const stepperCtx = useStepper()\n const {\n setActiveStep,\n activeStep,\n registerTrigger,\n triggerNodes,\n focusNext,\n focusPrev,\n focusFirst,\n focusLast,\n } = stepperCtx\n const { step, isDisabled } = useStepItem()\n const isSelected = activeStep === step\n const id = `stepper-tab-${step}`\n const panelId = `stepper-panel-${step}`\n\n // Register this trigger for keyboard navigation\n const btnRef = useRef(null)\n useEffect(() => {\n if (btnRef.current) {\n registerTrigger(btnRef.current)\n }\n }, [btnRef.current])\n\n // Find our index among triggers for navigation\n const myIdx = useMemo(\n () =>\n triggerNodes.findIndex((n: HTMLButtonElement) => n === btnRef.current),\n [triggerNodes, btnRef.current]\n )\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n switch (e.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n e.preventDefault()\n if (myIdx !== -1 && focusNext) focusNext(myIdx)\n break\n case \"ArrowLeft\":\n case \"ArrowUp\":\n e.preventDefault()\n if (myIdx !== -1 && focusPrev) focusPrev(myIdx)\n break\n case \"Home\":\n e.preventDefault()\n if (focusFirst) focusFirst()\n break\n case \"End\":\n e.preventDefault()\n if (focusLast) focusLast()\n break\n case \"Enter\":\n case \" \":\n e.preventDefault()\n setActiveStep(step)\n break\n }\n }\n\n // `asChild` composes onto the consumer's element via Slot, so the trigger\n // keeps its ref, tab semantics, and keyboard handlers either way.\n const Comp = asChild ? Slot.Root : \"button\"\n\n return (\n setActiveStep(step)}\n onKeyDown={handleKeyDown}\n disabled={isDisabled}\n {...props}\n >\n {children}\n \n )\n}\n\nfunction StepperIndicator({\n children,\n className,\n}: React.ComponentProps<\"div\">) {\n const { state, isLoading } = useStepItem()\n const { indicators } = useStepper()\n\n return (\n \n
\n {indicators &&\n ((isLoading && indicators.loading) ||\n (state === \"completed\" && indicators.completed) ||\n (state === \"active\" && indicators.active) ||\n (state === \"inactive\" && indicators.inactive))\n ? (isLoading && indicators.loading) ||\n (state === \"completed\" && indicators.completed) ||\n (state === \"active\" && indicators.active) ||\n (state === \"inactive\" && indicators.inactive)\n : children}\n
\n \n )\n}\n\nfunction StepperSeparator({ className }: React.ComponentProps<\"div\">) {\n const { state } = useStepItem()\n\n return (\n \n )\n}\n\nfunction StepperTitle({ children, className }: React.ComponentProps<\"h3\">) {\n const { state } = useStepItem()\n\n return (\n \n {children}\n \n )\n}\n\nfunction StepperDescription({\n children,\n className,\n}: React.ComponentProps<\"div\">) {\n const { state } = useStepItem()\n\n return (\n \n {children}\n \n )\n}\n\nfunction StepperNav({ children, className }: React.ComponentProps<\"nav\">) {\n const { activeStep, orientation } = useStepper()\n\n return (\n \n {children}\n \n )\n}\n\nfunction StepperPanel({ children, className }: React.ComponentProps<\"div\">) {\n const { activeStep } = useStepper()\n\n return (\n \n {children}\n \n )\n}\n\ninterface StepperContentProps extends React.ComponentProps<\"div\"> {\n value: number\n forceMount?: boolean\n}\n\nfunction StepperContent({\n value,\n forceMount,\n children,\n className,\n}: StepperContentProps) {\n const { activeStep } = useStepper()\n const isActive = value === activeStep\n\n if (!forceMount && !isActive) {\n return null\n }\n\n return (\n