import * as React from 'react'; import { Check, X } from 'lucide-react'; import { cn } from '../../shared/utils'; interface StepperContextValue { currentStep: number; totalSteps: number; orientation: 'horizontal' | 'vertical'; } const StepperContext = React.createContext(undefined); const useStepperContext = () => { const context = React.useContext(StepperContext); if (!context) throw new Error('useStepperContext must be used within a Stepper'); return context; }; interface StepperProps extends React.HTMLAttributes { currentStep: number; /** Layout direction. @default "horizontal" */ orientation?: 'horizontal' | 'vertical'; } /** * Multi-step progress indicator for sequential forms and wizards. * * @description * Guides the user through a linear multi-step flow. Supports horizontal (default) * and vertical orientations. Steps before `currentStep` show as completed (checkmark), * the current step as active (primary), error steps show an X, and future steps as pending (muted). * * @ai-rules * 1. `step` props are 1-indexed — the first step is `step={1}`, not `step={0}`. * 2. Control the active step with `currentStep` on `` (1-indexed). * 3. Validate the current step's form data BEFORE advancing `currentStep`. * 4. Use `error={true}` on a `` to indicate a validation failure on that step. */ const Stepper = React.forwardRef( ({ currentStep, orientation = 'horizontal', className, children, ...props }, ref) => { const totalSteps = React.Children.count(children); return (
{children}
); } ); Stepper.displayName = 'Stepper'; interface StepProps extends React.HTMLAttributes { step: number; label: string; description?: string; /** Marks this step as having a validation error. Renders a red X icon. */ error?: boolean; } const Step = React.forwardRef( ({ step, label, description, error = false, className, ...props }, ref) => { const { currentStep, totalSteps, orientation } = useStepperContext(); const isActive = step === currentStep; const isCompleted = step < currentStep && !error; const isFirst = step === 1; const isLast = step === totalSteps; const circleClasses = cn( 'relative flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full border-2 transition-colors', isActive && !error && 'border-primary bg-primary text-primary-foreground', isCompleted && 'border-primary bg-primary text-primary-foreground', error && 'border-destructive bg-destructive text-destructive-foreground', !isActive && !isCompleted && !error && 'border-muted bg-background text-muted-foreground' ); const connectorClasses = (filled: boolean) => cn('transition-colors', filled ? 'bg-primary' : 'bg-muted'); if (orientation === 'vertical') { return (
{/* Left column: circle + vertical line */}
{error ? ( ) : isCompleted ? ( ) : ( {step} )}
{!isLast && (
)}
{/* Right column: label + description */}
{label}
{description &&
{description}
}
); } // Horizontal layout (default) return (
{/* Circle and connector */}
{step > 1 && (
)}
{error ? ( ) : isCompleted ? ( ) : ( {step} )}
{step < totalSteps && (
)}
{/* Label */}
{label}
{description &&
{description}
}
); } ); Step.displayName = 'Step'; export { Stepper, Step, useStepperContext };