{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "stepper", "title": "Stepper", "description": "Multi-step progress indicator for guiding users through sequential workflows.", "dependencies": [ "motion", "react-use-measure" ], "files": [ { "path": "packages/ui/src/stepper.tsx", "content": "\"use client\";\n\nimport {\n AnimatePresence,\n MotionConfig,\n motion,\n useReducedMotion,\n} from \"motion/react\";\nimport * as React from \"react\";\nimport useMeasure from \"react-use-measure\";\n\nimport { cn } from \"./utils\";\n\n// =============================================================================\n// Stepper Context\n// =============================================================================\ninterface StepperContextValue {\n /** Direction of the last transition: +1 forward, -1 backward. */\n direction: number;\n /** Navigate to the next step. */\n goNext: () => void;\n /** Navigate to the previous step. */\n goPrevious: () => void;\n /** Navigate to a specific step index. */\n goTo: (step: number) => void;\n /** Whether the current step is the first step. */\n isFirst: boolean;\n /** Whether the current step is the last step. */\n isLast: boolean;\n /** Total number of steps. */\n totalSteps: number;\n /** Current step index (0-based). */\n value: number;\n}\n\nconst StepperContext = React.createContext(\n undefined\n);\n\n/**\n * Hook to access the stepper state and navigation methods.\n *\n * Must be used within a `` component.\n */\nfunction useStepper(): StepperContextValue {\n const context = React.useContext(StepperContext);\n if (context === undefined) {\n throw new Error(\"useStepper must be used within a component.\");\n }\n return context;\n}\n\n// =============================================================================\n// Stepper (Root)\n// =============================================================================\nexport interface StepperProps {\n children: React.ReactNode;\n /** Callback when the step changes. */\n onValueChange: (value: number) => void;\n /** Current step index (0-based). */\n value: number;\n}\n\nfunction Stepper({ value, onValueChange, children }: StepperProps) {\n const [direction, setDirection] = React.useState(1);\n const previousValue = React.useRef(value);\n\n // Count StepperStep children to determine totalSteps.\n // We walk the tree looking inside StepperContent for StepperStep children.\n const totalSteps = React.useMemo(() => {\n let count = 0;\n const countSteps = (node: React.ReactNode) => {\n React.Children.forEach(node, (child) => {\n if (React.isValidElement(child) && child.type === StepperStep) {\n count++;\n } else if (React.isValidElement(child)) {\n const props = child.props as { children?: React.ReactNode };\n if (props.children) {\n countSteps(props.children);\n }\n }\n });\n };\n countSteps(children);\n return count;\n }, [children]);\n\n // Track direction based on value changes\n React.useEffect(() => {\n if (value !== previousValue.current) {\n setDirection(value > previousValue.current ? 1 : -1);\n previousValue.current = value;\n }\n }, [value]);\n\n const goTo = React.useCallback(\n (step: number) => {\n const clamped = Math.max(0, Math.min(totalSteps - 1, step));\n onValueChange(clamped);\n },\n [totalSteps, onValueChange]\n );\n\n const goNext = React.useCallback(() => {\n goTo(value + 1);\n }, [goTo, value]);\n\n const goPrevious = React.useCallback(() => {\n goTo(value - 1);\n }, [goTo, value]);\n\n const contextValue = React.useMemo(\n () => ({\n value,\n totalSteps,\n direction,\n goTo,\n goNext,\n goPrevious,\n isFirst: value === 0,\n isLast: value === totalSteps - 1,\n }),\n [value, totalSteps, direction, goTo, goNext, goPrevious]\n );\n\n return (\n \n {children}\n \n );\n}\n\n// =============================================================================\n// StepperContent (Animated container)\n// =============================================================================\nconst stepVariants = {\n initial: (direction: number) => ({\n x: `${110 * direction}%`,\n opacity: 0,\n }),\n active: { x: \"0%\", opacity: 1 },\n exit: (direction: number) => ({\n x: `${-110 * direction}%`,\n opacity: 0,\n }),\n};\n\nconst reducedMotionVariants = {\n initial: { opacity: 0 },\n active: { opacity: 1 },\n exit: { opacity: 0 },\n};\n\nexport interface StepperContentProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction StepperContent({ className, children }: StepperContentProps) {\n const { value, direction } = useStepper();\n const shouldReduceMotion = useReducedMotion();\n const [ref, bounds] = useMeasure();\n\n const variants = shouldReduceMotion ? reducedMotionVariants : stepVariants;\n const customValue = shouldReduceMotion ? undefined : direction;\n\n // Extract only StepperStep children and pick the active one\n const steps: React.ReactNode[] = [];\n React.Children.forEach(children, (child) => {\n if (React.isValidElement(child) && child.type === StepperStep) {\n steps.push(child);\n }\n });\n\n const activeStep = steps[value] ?? null;\n\n return (\n \n 0 ? bounds.height : \"auto\" }}\n className={cn(\"relative overflow-hidden\", className)}\n data-slot=\"stepper-content\"\n >\n
\n \n \n {activeStep}\n \n \n
\n \n \n );\n}\n\n// =============================================================================\n// StepperStep (Individual step wrapper)\n// =============================================================================\nexport interface StepperStepProps extends React.ComponentProps<\"div\"> {}\n\nfunction StepperStep({ className, ...props }: StepperStepProps) {\n return
;\n}\n\n// =============================================================================\n// Exports\n// =============================================================================\nexport { Stepper, StepperContent, StepperStep, useStepper };\n", "type": "registry:ui" } ], "type": "registry:ui" }