{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","type":"registry:ui","title":"Stepper","description":"","dependencies":["@base-ui/react","cn"],"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 { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\n\nimport { cn } from \"cn\"\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<StepperContextValue | undefined>(undefined)\nconst StepItemContext = createContext<StepItemContextValue | undefined>(\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<HTMLDivElement> {\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<HTMLButtonElement[]>([])\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<StepperContextValue>(\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    <StepperContext.Provider value={contextValue}>\n      <div\n        role=\"tablist\"\n        aria-orientation={orientation}\n        data-slot=\"stepper\"\n        className={cn(\"w-full\", className)}\n        data-orientation={orientation}\n        {...props}\n      >\n        {children}\n      </div>\n    </StepperContext.Provider>\n  )\n}\n\ninterface StepperItemProps extends React.HTMLAttributes<HTMLDivElement> {\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    <StepItemContext.Provider\n      value={{ step, state, isDisabled: disabled, isLoading }}\n    >\n      <div\n        data-slot=\"stepper-item\"\n        className={cn(\n          \"group/step flex items-center justify-center not-last:flex-1 group-data-[orientation=horizontal]/stepper-nav:flex-row group-data-[orientation=vertical]/stepper-nav:flex-col\",\n          className\n        )}\n        data-state={state}\n        {...(isLoading ? { \"data-loading\": true } : {})}\n        {...props}\n      >\n        {children}\n      </div>\n    </StepItemContext.Provider>\n  )\n}\n\ntype StepperTriggerProps = useRender.ComponentProps<\"button\">\n\nfunction StepperTrigger({\n  className,\n  children,\n  tabIndex,\n  render,\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<HTMLButtonElement>(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<HTMLButtonElement>) => {\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  const defaultProps = {\n    role: \"tab\",\n    id,\n    \"aria-selected\": isSelected,\n    \"aria-controls\": panelId,\n    tabIndex: typeof tabIndex === \"number\" ? tabIndex : isSelected ? 0 : -1,\n    \"data-slot\": \"stepper-trigger\",\n    \"data-state\": state,\n    \"data-loading\": isLoading,\n    className: cn(\n      \"focus-visible:border-ring focus-visible:ring-ring/50 inline-flex cursor-pointer items-center outline-none focus-visible:z-10 focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-60\",\n      \"gap-2.5 rounded-full\",\n      className\n    ),\n    onClick: () => setActiveStep(step),\n    onKeyDown: handleKeyDown,\n    disabled: isDisabled,\n    children,\n  }\n\n  return useRender({\n    defaultTagName: \"button\",\n    render,\n    ref: btnRef,\n    props: mergeProps<\"button\">(defaultProps, props),\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    <div\n      data-slot=\"stepper-indicator\"\n      data-state={state}\n      className={cn(\n        \"border-background bg-accent text-accent-foreground data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground data-[state=active]:bg-primary data-[state=active]:text-primary-foreground relative flex size-6 shrink-0 items-center justify-center overflow-hidden\",\n        \"rounded-full text-xs\",\n        className\n      )}\n    >\n      <div className=\"absolute\">\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      </div>\n    </div>\n  )\n}\n\nfunction StepperSeparator({ className }: React.ComponentProps<\"div\">) {\n  const { state } = useStepItem()\n\n  return (\n    <div\n      data-slot=\"stepper-separator\"\n      data-state={state}\n      className={cn(\n        \"bg-muted rounded-sm m-0.5 group-data-[orientation=horizontal]/stepper-nav:h-0.5 group-data-[orientation=horizontal]/stepper-nav:flex-1 group-data-[orientation=vertical]/stepper-nav:h-12 group-data-[orientation=vertical]/stepper-nav:w-0.5\",\n        className\n      )}\n    />\n  )\n}\n\nfunction StepperTitle({ children, className }: React.ComponentProps<\"h3\">) {\n  const { state } = useStepItem()\n\n  return (\n    <h3\n      data-slot=\"stepper-title\"\n      data-state={state}\n      className={cn(\"text-sm leading-none font-medium\", className)}\n    >\n      {children}\n    </h3>\n  )\n}\n\nfunction StepperDescription({\n  children,\n  className,\n}: React.ComponentProps<\"div\">) {\n  const { state } = useStepItem()\n\n  return (\n    <div\n      data-slot=\"stepper-description\"\n      data-state={state}\n      className={cn(\"text-muted-foreground text-sm\", className)}\n    >\n      {children}\n    </div>\n  )\n}\n\nfunction StepperNav({ children, className }: React.ComponentProps<\"nav\">) {\n  const { activeStep, orientation } = useStepper()\n\n  return (\n    <nav\n      data-slot=\"stepper-nav\"\n      data-state={activeStep}\n      data-orientation={orientation}\n      className={cn(\n        \"group/stepper-nav inline-flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col\",\n        className\n      )}\n    >\n      {children}\n    </nav>\n  )\n}\n\nfunction StepperPanel({ children, className }: React.ComponentProps<\"div\">) {\n  const { activeStep } = useStepper()\n\n  return (\n    <div\n      data-slot=\"stepper-panel\"\n      data-state={activeStep}\n      className={cn(\"w-full\", className)}\n    >\n      {children}\n    </div>\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    <div\n      data-slot=\"stepper-content\"\n      data-state={activeStep}\n      className={cn(\"w-full\", className, !isActive && forceMount && \"hidden\")}\n      hidden={!isActive && forceMount}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport {\n  useStepper,\n  useStepItem,\n  Stepper,\n  StepperItem,\n  StepperTrigger,\n  StepperIndicator,\n  StepperSeparator,\n  StepperTitle,\n  StepperDescription,\n  StepperPanel,\n  StepperContent,\n  StepperNav,\n  type StepperProps,\n  type StepperItemProps,\n  type StepperTriggerProps,\n  type StepperContentProps,\n}","target":"components/reui/stepper.tsx"}]}