import { Slot } from '@radix-ui/react-slot' import * as Stepperize from '@stepperize/react' import { cva } from 'class-variance-authority' import * as React from 'react' import type { VariantProps } from 'class-variance-authority' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' const StepperContext = React.createContext(null) const useStepperProvider = (): Stepper.ConfigProps => { const context = React.useContext(StepperContext) if (!context) { throw new Error('useStepper must be used within a StepperProvider.') } return context } const defineStepper = >( ...steps: Steps ): Stepper.DefineProps => { const { Scoped, useStepper, ...rest } = Stepperize.defineStepper(...steps) const StepperContainer = ({ children, className, ...props }: Omit, 'children'> & { children: | React.ReactNode | ((props: { methods: Stepperize.Stepper }) => React.ReactNode) }) => { const methods = useStepper() return (
{typeof children === 'function' ? children({ methods }) : children}
) } return { ...rest, useStepper, Stepper: { Provider: ({ variant = 'horizontal', labelOrientation = 'horizontal', tracking = false, children, className, ...props }) => { // Avoid leaking non-DOM props like `initialStep` onto the div const { initialStep, initialMetadata, ...restProps } = props as { initialStep?: any initialMetadata?: any } & Record return ( {children} ) }, Navigation: ({ children, 'aria-label': ariaLabel = 'Stepper Navigation', ...props }) => { const { variant } = useStepperProvider() return ( ) }, Step: ({ children, className, icon, ...props }) => { const { variant, labelOrientation } = useStepperProvider() const { current } = useStepper() const utils = rest.utils const steps = rest.steps const stepIndex = utils.getIndex(props.of) const step = steps[stepIndex] const currentIndex = utils.getIndex(current.id) const isLast = utils.getLast().id === props.of const isActive = current.id === props.of const dataState = getStepState(currentIndex, stepIndex) const childMap = useStepChildren(children) const title = childMap.get('title') const description = childMap.get('description') const panel = childMap.get('panel') if (variant === 'circle') { return (
  • {title} {description}
  • ) } return ( <>
  • {variant === 'horizontal' && labelOrientation === 'vertical' && ( )}
    {title} {description}
  • {variant === 'horizontal' && labelOrientation === 'horizontal' && ( )} {variant === 'vertical' && (
    {!isLast && (
    )}
    {panel}
    )} ) }, Title, Description, Panel: ({ children, asChild, ...props }) => { const Comp = asChild ? Slot : 'div' const { tracking } = useStepperProvider() return ( scrollIntoStepperPanel(node, tracking)} {...props} > {children} ) }, Controls: ({ children, className, asChild, ...props }) => { const Comp = asChild ? Slot : 'div' return ( {children} ) }, }, } } const Title = ({ children, className, asChild, ...props }: React.ComponentProps<'h4'> & { asChild?: boolean }) => { const Comp = asChild ? Slot : 'h4' return ( {children} ) } const Description = ({ children, className, asChild, ...props }: React.ComponentProps<'p'> & { asChild?: boolean }) => { const Comp = asChild ? Slot : 'p' return ( {children} ) } const StepperSeparator = ({ orientation, isLast, labelOrientation, state, disabled, }: { isLast: boolean state: string disabled?: boolean } & VariantProps) => { if (isLast) { return null } return (
    ) } const CircleStepIndicator = ({ currentStep, totalSteps, size = 80, strokeWidth = 6, }: Stepper.CircleStepIndicatorProps) => { const radius = (size - strokeWidth) / 2 const circumference = radius * 2 * Math.PI const fillPercentage = (currentStep / totalSteps) * 100 const dashOffset = circumference - (circumference * fillPercentage) / 100 return (
    Step Indicator
    {currentStep} of {totalSteps}
    ) } const classForNavigationList = cva('flex gap-2', { variants: { variant: { horizontal: 'flex-row items-center justify-between', vertical: 'flex-col', circle: 'flex-row items-center justify-between', }, }, }) const classForSeparator = cva( [ 'bg-muted', 'data-[state=completed]:bg-primary data-[disabled]:opacity-50', 'transition-all duration-300 ease-in-out', ], { variants: { orientation: { horizontal: 'h-0.5 flex-1', vertical: 'h-full w-0.5', }, labelOrientation: { vertical: 'absolute top-5 right-[calc(-50%+20px)] left-[calc(50%+30px)] block shrink-0', }, }, }, ) function scrollIntoStepperPanel( node: HTMLDivElement | null, tracking?: boolean, ) { if (tracking) { node?.scrollIntoView({ behavior: 'smooth', block: 'center' }) } } const useStepChildren = (children: React.ReactNode) => { return React.useMemo(() => extractChildren(children), [children]) } const extractChildren = (children: React.ReactNode) => { const childrenArray = React.Children.toArray(children) const map = new Map() for (const child of childrenArray) { if (React.isValidElement(child)) { if (child.type === Title) { map.set('title', child) } else if (child.type === Description) { map.set('description', child) } else { map.set('panel', child) } } } return map } const onStepKeyDown = ( e: React.KeyboardEvent, nextStep: Stepperize.Step, prevStep: Stepperize.Step, ) => { const { key } = e const directions = { next: ['ArrowRight', 'ArrowDown'], prev: ['ArrowLeft', 'ArrowUp'], } if (directions.next.includes(key) || directions.prev.includes(key)) { const direction = directions.next.includes(key) ? 'next' : 'prev' const step = direction === 'next' ? nextStep : prevStep if (!step) { return } const stepElement = document.getElementById(`step-${step.id}`) if (!stepElement) { return } const isActive = stepElement.parentElement?.getAttribute('data-state') !== 'inactive' if (isActive || direction === 'prev') { stepElement.focus() } } } const getStepState = (currentIndex: number, stepIndex: number) => { if (currentIndex === stepIndex) { return 'active' } if (currentIndex > stepIndex) { return 'completed' } return 'inactive' } namespace Stepper { export type StepperVariant = 'horizontal' | 'vertical' | 'circle' export type StepperLabelOrientation = 'horizontal' | 'vertical' export type ConfigProps = { variant?: StepperVariant labelOrientation?: StepperLabelOrientation tracking?: boolean } export type DefineProps> = Omit< Stepperize.StepperReturn, 'Scoped' > & { Stepper: { Provider: ( props: Omit, 'children'> & Omit, 'children'> & Stepper.ConfigProps & { children: | React.ReactNode | ((props: { methods: Stepperize.Stepper }) => React.ReactNode) }, ) => React.ReactElement Navigation: (props: React.ComponentProps<'nav'>) => React.ReactElement Step: ( props: React.ComponentProps<'button'> & { of: Stepperize.Get.Id icon?: React.ReactNode }, ) => React.ReactElement Title: (props: AsChildProps<'h4'>) => React.ReactElement Description: (props: AsChildProps<'p'>) => React.ReactElement Panel: (props: AsChildProps<'div'>) => React.ReactElement Controls: (props: AsChildProps<'div'>) => React.ReactElement } } export type CircleStepIndicatorProps = { currentStep: number totalSteps: number size?: number strokeWidth?: number } } type AsChildProps = React.ComponentProps & { asChild?: boolean } export { defineStepper }