{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stepper","title":"Stepper","description":"A keyboard-navigable multi-step flow with flexible triggers and panels.","files":[{"path":"ui/stepper.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype StepperOrientation = \"horizontal\" | \"vertical\";\ntype StepState = \"active\" | \"completed\" | \"inactive\";\ntype StepIndicators = Partial<Record<StepState | \"loading\", React.ReactNode>>;\n\ntype StepperContextValue = {\n  activeStep: number;\n  setActiveStep: (step: number) => void;\n  stepsCount: number;\n  orientation: StepperOrientation;\n  indicators: StepIndicators;\n  triggerNodes: HTMLButtonElement[];\n  registerTrigger: (node: HTMLButtonElement) => () => void;\n  focusNext: (currentIndex: number) => void;\n  focusPrevious: (currentIndex: number) => void;\n  focusFirst: () => void;\n  focusLast: () => void;\n};\n\ntype StepItemContextValue = {\n  step: number;\n  state: StepState;\n  isDisabled: boolean;\n  isLoading: boolean;\n};\n\ntype StepperProps = React.ComponentProps<\"div\"> & {\n  defaultValue?: number;\n  value?: number;\n  onValueChange?: (value: number) => void;\n  orientation?: StepperOrientation;\n  indicators?: StepIndicators;\n};\n\ntype StepperItemProps = React.ComponentProps<\"div\"> & {\n  step: number;\n  completed?: boolean;\n  disabled?: boolean;\n  loading?: boolean;\n};\n\ntype StepperTriggerState = Record<string, unknown> & {\n  state: StepState;\n  isLoading: boolean;\n  isSelected: boolean;\n};\n\ntype StepperTriggerProps = Omit<React.ComponentProps<\"button\">, \"children\"> & {\n  render?: useRender.RenderProp<StepperTriggerState>;\n  children?: React.ReactNode;\n};\n\ntype StepperContentProps = React.ComponentProps<\"div\"> & {\n  value: number;\n  forceMount?: boolean;\n};\n\nconst defaultIndicators = {};\nconst StepperContext = React.createContext<StepperContextValue | null>(null);\nconst StepItemContext = React.createContext<StepItemContextValue | null>(null);\n\nfunction Stepper({\n  defaultValue = 1,\n  value,\n  onValueChange,\n  orientation = \"horizontal\",\n  indicators = defaultIndicators,\n  className,\n  children,\n  ...props\n}: StepperProps) {\n  const [uncontrolledStep, setUncontrolledStep] = React.useState(defaultValue);\n  const [triggerNodes, setTriggerNodes] = React.useState<HTMLButtonElement[]>([]);\n  const activeStep = value ?? uncontrolledStep;\n\n  const setActiveStep = React.useCallback(\n    (step: number) => {\n      if (value === undefined) {\n        setUncontrolledStep(step);\n      }\n\n      onValueChange?.(step);\n    },\n    [onValueChange, value],\n  );\n\n  const registerTrigger = React.useCallback((node: HTMLButtonElement) => {\n    setTriggerNodes((currentNodes) =>\n      currentNodes.includes(node) ? currentNodes : [...currentNodes, node],\n    );\n\n    return () => {\n      setTriggerNodes((currentNodes) => currentNodes.filter((currentNode) => currentNode !== node));\n    };\n  }, []);\n\n  const focusTrigger = React.useCallback(\n    (index: number) => {\n      triggerNodes[index]?.focus();\n    },\n    [triggerNodes],\n  );\n\n  const focusNext = React.useCallback(\n    (currentIndex: number) => {\n      if (triggerNodes.length === 0) {\n        return;\n      }\n\n      focusTrigger((currentIndex + 1) % triggerNodes.length);\n    },\n    [focusTrigger, triggerNodes.length],\n  );\n\n  const focusPrevious = React.useCallback(\n    (currentIndex: number) => {\n      if (triggerNodes.length === 0) {\n        return;\n      }\n\n      focusTrigger((currentIndex - 1 + triggerNodes.length) % triggerNodes.length);\n    },\n    [focusTrigger, triggerNodes.length],\n  );\n\n  const focusFirst = React.useCallback(() => focusTrigger(0), [focusTrigger]);\n  const focusLast = React.useCallback(\n    () => focusTrigger(triggerNodes.length - 1),\n    [focusTrigger, triggerNodes.length],\n  );\n\n  const contextValue = React.useMemo<StepperContextValue>(\n    () => ({\n      activeStep,\n      setActiveStep,\n      stepsCount: countStepperItems(children),\n      orientation,\n      indicators,\n      triggerNodes,\n      registerTrigger,\n      focusNext,\n      focusPrevious,\n      focusFirst,\n      focusLast,\n    }),\n    [\n      activeStep,\n      children,\n      focusFirst,\n      focusLast,\n      focusNext,\n      focusPrevious,\n      indicators,\n      orientation,\n      registerTrigger,\n      setActiveStep,\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        data-orientation={orientation}\n        className={cn(\"w-full\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </StepperContext.Provider>\n  );\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  const state = getStepState({ completed, step, activeStep });\n  const isLoading = loading && step === activeStep;\n  const contextValue = React.useMemo<StepItemContextValue>(\n    () => ({ step, state, isDisabled: disabled, isLoading }),\n    [disabled, isLoading, state, step],\n  );\n\n  return (\n    <StepItemContext.Provider value={contextValue}>\n      <div\n        data-slot=\"stepper-item\"\n        data-state={state}\n        data-disabled={disabled ? \"\" : undefined}\n        data-loading={isLoading ? \"\" : undefined}\n        className={cn(\n          \"group/step group-data-[orientation=horizontal]/stepper-nav:contents group-data-[orientation=vertical]/stepper-nav:flex group-data-[orientation=vertical]/stepper-nav:flex-col group-data-[orientation=vertical]/stepper-nav:items-center\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </StepItemContext.Provider>\n  );\n}\n\nfunction StepperTrigger({ render, className, children, tabIndex, ...props }: StepperTriggerProps) {\n  const { step, state, isDisabled, isLoading } = useStepItem();\n  const {\n    activeStep,\n    setActiveStep,\n    registerTrigger,\n    triggerNodes,\n    focusNext,\n    focusPrevious,\n    focusFirst,\n    focusLast,\n  } = useStepper();\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const isSelected = activeStep === step;\n  const triggerId = getTriggerId(step);\n  const panelId = getPanelId(step);\n\n  React.useEffect(() => {\n    const triggerNode = triggerRef.current;\n\n    if (!triggerNode) {\n      return undefined;\n    }\n\n    return registerTrigger(triggerNode);\n  }, [registerTrigger]);\n\n  const currentIndex = triggerRef.current ? triggerNodes.indexOf(triggerRef.current) : -1;\n  const triggerState = React.useMemo<StepperTriggerState>(\n    () => ({ state, isLoading, isSelected }),\n    [isLoading, isSelected, state],\n  );\n  const defaultProps = {\n    type: \"button\" as const,\n    role: \"tab\" as const,\n    id: triggerId,\n    \"aria-selected\": isSelected,\n    \"aria-controls\": panelId,\n    tabIndex: getTriggerTabIndex(tabIndex, isSelected),\n    \"data-slot\": \"stepper-trigger\",\n    \"data-state\": state,\n    \"data-loading\": isLoading ? \"\" : undefined,\n    className: cn(\n      \"inline-flex items-center gap-3 rounded-full outline-none select-none focus-visible:z-10 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-60\",\n      className,\n    ),\n    disabled: isDisabled,\n    children,\n    onClick: () => setActiveStep(step),\n    onKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      switch (event.key) {\n        case \"ArrowRight\":\n        case \"ArrowDown\":\n          event.preventDefault();\n          if (currentIndex !== -1) {\n            focusNext(currentIndex);\n          }\n          break;\n        case \"ArrowLeft\":\n        case \"ArrowUp\":\n          event.preventDefault();\n          if (currentIndex !== -1) {\n            focusPrevious(currentIndex);\n          }\n          break;\n        case \"Home\":\n          event.preventDefault();\n          focusFirst();\n          break;\n        case \"End\":\n          event.preventDefault();\n          focusLast();\n          break;\n        case \"Enter\":\n        case \" \":\n          event.preventDefault();\n          setActiveStep(step);\n          break;\n      }\n    },\n  };\n\n  return useRender({\n    defaultTagName: \"button\",\n    render,\n    ref: triggerRef,\n    state: triggerState,\n    props: mergeProps<\"button\">(defaultProps, props),\n  });\n}\n\nfunction StepperIndicator({ children, className, ...props }: React.ComponentProps<\"div\">) {\n  const { state, isLoading } = useStepItem();\n  const { indicators } = useStepper();\n  const indicator = isLoading ? indicators.loading : indicators[state];\n\n  return (\n    <div\n      data-slot=\"stepper-indicator\"\n      data-state={state}\n      data-loading={isLoading ? \"\" : undefined}\n      className={cn(\n        \"relative flex size-8 shrink-0 cursor-default items-center justify-center rounded-full border-2 border-muted-foreground/40 text-sm font-medium text-muted-foreground\",\n        \"data-[state=active]:border-primary data-[state=active]:bg-primary data-[state=active]:text-primary-foreground\",\n        \"data-[state=completed]:border-primary data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"absolute\">{indicator ?? children}</span>\n    </div>\n  );\n}\n\nfunction StepperSeparator({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"stepper-separator\"\n      className={cn(\n        \"rounded-full bg-muted\",\n        \"group-data-[orientation=horizontal]/stepper-nav:mx-3 group-data-[orientation=horizontal]/stepper-nav:h-0.5\",\n        \"group-data-[orientation=vertical]/stepper-nav:mx-2 group-data-[orientation=vertical]/stepper-nav:h-12 group-data-[orientation=vertical]/stepper-nav:w-0.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction StepperTitle({ className, ...props }: React.ComponentProps<\"span\">) {\n  const { state } = useStepItem();\n\n  return (\n    <span\n      data-slot=\"stepper-title\"\n      data-state={state}\n      className={cn(\"text-sm leading-none font-medium\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction StepperDescription({ className, ...props }: 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-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction StepperNav({ children, className, style, ...props }: React.ComponentProps<\"nav\">) {\n  const { activeStep, orientation, stepsCount } = useStepper();\n  const gridTemplateColumns =\n    orientation === \"horizontal\" && stepsCount > 0\n      ? Array.from({ length: stepsCount }, (_, index) =>\n          index < stepsCount - 1 ? \"auto 1fr\" : \"auto\",\n        ).join(\" \")\n      : undefined;\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\",\n        \"data-[orientation=horizontal]:grid data-[orientation=horizontal]:w-full data-[orientation=horizontal]:items-center\",\n        \"data-[orientation=vertical]:inline-flex data-[orientation=vertical]:flex-col\",\n        className,\n      )}\n      style={{ ...style, gridTemplateColumns }}\n      {...props}\n    >\n      {children}\n    </nav>\n  );\n}\n\nfunction StepperPanel({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"stepper-panel\" className={cn(\"w-full\", className)} {...props} />;\n}\n\nfunction StepperContent({ value, forceMount, children, className, ...props }: 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      role=\"tabpanel\"\n      id={getPanelId(value)}\n      aria-labelledby={getTriggerId(value)}\n      data-slot=\"stepper-content\"\n      data-state={isActive ? \"active\" : \"inactive\"}\n      className={cn(\"w-full\", className, !isActive && forceMount && \"hidden\")}\n      hidden={!isActive && forceMount}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction useStepper() {\n  const ctx = React.useContext(StepperContext);\n\n  if (!ctx) {\n    throw new Error(\"useStepper must be used within <Stepper>.\");\n  }\n\n  return ctx;\n}\n\nfunction useStepItem() {\n  const ctx = React.useContext(StepItemContext);\n\n  if (!ctx) {\n    throw new Error(\"useStepItem must be used within <StepperItem>.\");\n  }\n\n  return ctx;\n}\n\nfunction countStepperItems(node: React.ReactNode): number {\n  let count = 0;\n\n  React.Children.forEach(node, (child) => {\n    if (!React.isValidElement(child)) {\n      return;\n    }\n\n    if (child.type === StepperItem) {\n      count++;\n    }\n\n    if (React.isValidElement<{ children?: React.ReactNode }>(child) && child.props.children) {\n      count += countStepperItems(child.props.children);\n    }\n  });\n\n  return count;\n}\n\nfunction getStepState({\n  completed,\n  step,\n  activeStep,\n}: {\n  completed: boolean;\n  step: number;\n  activeStep: number;\n}): StepState {\n  if (completed || step < activeStep) {\n    return \"completed\";\n  }\n\n  if (activeStep === step) {\n    return \"active\";\n  }\n\n  return \"inactive\";\n}\n\nfunction getTriggerTabIndex(\n  tabIndex: StepperTriggerProps[\"tabIndex\"],\n  isSelected: boolean,\n): number {\n  if (typeof tabIndex === \"number\") {\n    return tabIndex;\n  }\n\n  return isSelected ? 0 : -1;\n}\n\nfunction getTriggerId(step: number): string {\n  return `stepper-tab-${step}`;\n}\n\nfunction getPanelId(step: number): string {\n  return `stepper-panel-${step}`;\n}\n\nStepper.displayName = \"Stepper\";\nStepperItem.displayName = \"StepperItem\";\nStepperTrigger.displayName = \"StepperTrigger\";\nStepperIndicator.displayName = \"StepperIndicator\";\nStepperSeparator.displayName = \"StepperSeparator\";\nStepperTitle.displayName = \"StepperTitle\";\nStepperDescription.displayName = \"StepperDescription\";\nStepperNav.displayName = \"StepperNav\";\nStepperPanel.displayName = \"StepperPanel\";\nStepperContent.displayName = \"StepperContent\";\n\nexport {\n  Stepper,\n  StepperItem,\n  StepperTrigger,\n  StepperIndicator,\n  StepperSeparator,\n  StepperTitle,\n  StepperDescription,\n  StepperPanel,\n  StepperContent,\n  StepperNav,\n  useStepper,\n  useStepItem,\n  type StepperProps,\n  type StepperItemProps,\n  type StepperTriggerProps,\n  type StepperContentProps,\n};","target":"@ui/stepper.tsx"}],"type":"registry:ui","dependencies":["@base-ui/react"]}