{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "steps",
  "type": "registry:component",
  "title": "Steps",
  "description": "A flow that moves one stop at a time, its numbered stops joined by a line that fills as far as you have got.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [
    "https://panelui.dev/r/web/spinner.json"
  ],
  "files": [
    {
      "path": "components/panelui/steps/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/steps/index.tsx",
      "content": "'use client';\n\n/**\n * Steps — a flow that moves one stop at a time: a checkout, a wizard, an\n * onboarding, anything with a beginning, a middle and an end.\n *\n * ```tsx\n * <Steps defaultValue={1}>\n *   <Steps.Item step={0}>\n *     <Steps.Trigger>\n *       <Steps.Indicator />\n *       <Steps.Title>Basket</Steps.Title>\n *     </Steps.Trigger>\n *   </Steps.Item>\n *   <Steps.Item step={1}>\n *     <Steps.Trigger>\n *       <Steps.Indicator />\n *       <Steps.Title>Delivery</Steps.Title>\n *     </Steps.Trigger>\n *   </Steps.Item>\n *   <Steps.Item step={2}>\n *     <Steps.Trigger>\n *       <Steps.Indicator />\n *       <Steps.Title>Payment</Steps.Title>\n *     </Steps.Trigger>\n *   </Steps.Item>\n * </Steps>\n * ```\n *\n * ## The state is worked out, not handed in\n *\n * An item knows whether it is complete, current or still to come by comparing\n * its own `step` with the root's active one. There is nothing to keep in sync\n * and no way to write a step that is both done and ahead: the circle, the\n * label and the connector after it all read the same answer, which is why a\n * flow of four steps cannot end up with two of them lit.\n *\n * The root decides which step is active. Pass `value` to hold it yourself, or\n * `defaultValue` to let the stepper keep its own, and `onValueChange` to hear\n * about every move. `Steps.Trigger` moves it; without a trigger an item is a\n * label with no button in it.\n *\n * ## The connectors are the component's job\n *\n * The root counts the items it holds, so a step with one after it draws the\n * connector to it — there is no separator to forget, mis-order, or leave\n * hanging past the last stop. The line fills as far as the flow has got: the\n * segment after a completed step is primary, the segment waiting for the step\n * ahead is muted. `separators={false}` turns the automatic ones off for a\n * stepper that wants none at all. An item holding its own `Steps.Separator`\n * keeps it and is given no second one, so a connector can still be dressed by\n * hand.\n *\n * ## Going back, not skipping ahead\n *\n * A completed step is a button: it can be reached with Tab, pressed with\n * Enter or Space, and takes you back to where it is. The step the flow is on\n * would only press itself, so it is marked with `aria-current=\"step\"` instead\n * of being a control that does nothing, and a step still to come has not been\n * earned, so it is neither — no dead stops between the ones that work. A flow\n * that can be entered part-way says so with `reachable` on the item.\n *\n * ## A flow, not tabs\n *\n * These are not tablist semantics. The steps are a sequence moving forward,\n * so the current trigger carries `aria-current=\"step\"` and its name carries\n * its position — \"Payment, step 2 of 3\" — while a completed one says so. The\n * arrow keys do nothing: a row of tabs is a set to browse, a stepper is a\n * line to walk.\n *\n * ## Two directions\n *\n * `orientation=\"horizontal\"` runs left to right with the labels under the\n * circles and the connectors between them; `vertical` runs top to bottom with\n * the circle beside each label and the connector dropping to the next. The\n * rules for which state is which, and which steps can be pressed, are the same\n * either way.\n */\nimport {\n  Children,\n  cloneElement,\n  Fragment,\n  createContext,\n  isValidElement,\n  useContext,\n  useState,\n  type ComponentProps,\n  type MouseEvent as ReactMouseEvent,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport { Spinner } from '@/components/panelui/spinner';\nimport { deriveStepState, stepAnnouncement, type StepState, type StepsOrientation } from './steps-state';\n\nexport type { StepState, StepsOrientation };\n\n/** The one curve every state change here uses: quick enough to feel like a reply, slow enough to follow. */\nconst SETTLE = 'transition-[background-color,color,box-shadow,opacity] duration-200 ease-out motion-reduce:transition-none';\n\nconst stepsVariants = tv({\n  slots: {\n    root: 'flex w-full',\n    item: 'relative flex min-w-0',\n    trigger: [\n      'outline-none select-none',\n      SETTLE,\n      'focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n      '[&_svg]:pointer-events-none [&_svg]:shrink-0',\n    ],\n    indicator: [\n      'relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold tabular-nums',\n      SETTLE,\n    ],\n    title: ['text-sm font-medium leading-5', SETTLE].join(' '),\n    description: ['text-sm leading-5 text-muted-foreground', SETTLE].join(' '),\n    separator: [\n      'pointer-events-none z-0 rounded-full bg-border',\n      'transition-[background-color] duration-300 ease-out motion-reduce:transition-none',\n    ],\n  },\n  variants: {\n    orientation: {\n      /*\n       * Across: the label sits under the circle and the connector runs from\n       * the edge of one circle to the edge of the next, at circle height.\n       * The calc is half the item either side of its own circle.\n       */\n      horizontal: {\n        root: 'items-start',\n        item: 'flex-1 flex-col items-center',\n        trigger: 'grid grid-cols-1 justify-items-center gap-2 px-2 pb-1.5 text-center',\n        separator: 'absolute top-4 start-[calc(50%_+_1.25rem)] end-[calc(-50%_+_1.25rem)] h-0.5 -translate-y-1/2',\n      },\n      /*\n       * Down: the circle stays in a column of its own and the label sits\n       * beside it, so a description can run on under a title without moving\n       * the line. The connector drops from the circle's column to the next.\n       */\n      vertical: {\n        root: 'flex-col',\n        item: 'w-full flex-col items-start',\n        trigger: 'grid w-full grid-cols-[auto_minmax(0,1fr)] items-start gap-x-3 gap-y-1 pe-4 text-start',\n        indicator: 'row-span-2',\n        title: 'mt-1.5',\n        separator: 'ms-4 h-8 w-0.5',\n      },\n    },\n    state: {\n      upcoming: {\n        indicator: 'bg-muted text-muted-foreground ring-1 ring-border',\n        title: 'text-muted-foreground',\n      },\n      current: {\n        indicator: 'bg-primary text-primary-foreground ring-4 ring-primary/15',\n        title: 'text-foreground',\n      },\n      completed: {\n        indicator: 'bg-primary text-primary-foreground',\n        title: 'text-foreground',\n        separator: 'bg-primary',\n      },\n      loading: {\n        indicator: 'bg-primary/10 text-primary ring-4 ring-primary/15',\n        title: 'text-foreground',\n      },\n    },\n    clickable: {\n      true: { trigger: 'cursor-pointer rounded-xl hover:bg-muted/50' },\n      false: { trigger: 'cursor-default' },\n    },\n    disabled: {\n      true: { trigger: 'opacity-60' },\n    },\n  },\n  defaultVariants: { orientation: 'horizontal', state: 'upcoming', clickable: false, disabled: false },\n});\n\ninterface StepsContextValue {\n  activeStep: number;\n  setActiveStep: (step: number) => void;\n  orientation: StepsOrientation;\n  separators: boolean;\n}\n\ninterface StepItemContextValue {\n  step: number;\n  state: StepState;\n  filled: boolean;\n  loading: boolean;\n  disabled: boolean;\n  reachable: boolean;\n  completed: boolean;\n}\n\n/**\n * Where an item sits among its siblings, published by the root.\n *\n * Deliberately not the item's own `step`: that is the author's numbering of\n * the flow and may skip, repeat or start anywhere, while the connector and\n * the \"2 of 3\" announcement both need the position in the row as rendered.\n */\ninterface StepPlacementContextValue {\n  position: number;\n  total: number;\n}\n\nconst StepsContext = createContext<StepsContextValue | null>(null);\nconst StepItemContext = createContext<StepItemContextValue | null>(null);\nconst StepPlacementContext = createContext<StepPlacementContextValue | null>(null);\n\nfunction useSteps(component: string): StepsContextValue {\n  const context = useContext(StepsContext);\n  if (!context) throw new Error(`${component} must be used within a <Steps>`);\n  return context;\n}\n\nfunction useStepItem(component: string): StepItemContextValue {\n  const context = useContext(StepItemContext);\n  if (!context) throw new Error(`${component} must be used within a <Steps.Item>`);\n  return context;\n}\n\n/**\n * Runs the children through fragments, so a step wrapped in one still gets\n * its position. Only the top level is looked at, which is where a step has to\n * be: an item buried inside another component cannot be given a connector\n * that lines up with its siblings.\n */\nfunction flattenSteps(children: ReactNode, prefix = ''): ReactNode[] {\n  return Children.toArray(children).flatMap((node) => {\n    if (isValidElement<{ children?: ReactNode }>(node) && node.type === Fragment) {\n      return flattenSteps(node.props.children, `${prefix}${String(node.key ?? '')}/`);\n    }\n    if (prefix && isValidElement(node)) {\n      return [cloneElement(node, { key: `${prefix}${String(node.key ?? '')}` })];\n    }\n    return [node];\n  });\n}\n\nexport interface StepsProps extends Omit<ComponentProps<'div'>, 'defaultValue' | 'onChange'> {\n  /** The active step, when the caller holds it. */\n  value?: number;\n  /** The step it starts on, when the caller does not. Default `0`. */\n  defaultValue?: number;\n  /** Called with the step chosen, whether by a trigger or by your own controls. */\n  onValueChange?: (value: number) => void;\n  /** Which way the flow runs. Default `horizontal`. */\n  orientation?: StepsOrientation;\n  /**\n   * Draw the connector between one step and the next. On by default. An item\n   * holding its own `Steps.Separator` is left alone either way, so this is for\n   * a stepper that wants no connectors at all.\n   */\n  separators?: boolean;\n  /** Classes for the row, or column, that holds the steps. */\n  className?: string;\n  /** The steps, in order, and anything else that belongs in the row. */\n  children?: ReactNode;\n}\n\n/** Holds which step is active for every part inside it. */\nfunction StepsRoot({\n  value,\n  defaultValue = 0,\n  onValueChange,\n  orientation = 'horizontal',\n  separators = true,\n  className,\n  children,\n  ...props\n}: StepsProps) {\n  const [innerStep, setInnerStep] = useState(defaultValue);\n  const controlled = value !== undefined;\n  const activeStep = controlled ? value : innerStep;\n\n  const setActiveStep = (next: number) => {\n    if (!controlled) setInnerStep(next);\n    onValueChange?.(next);\n  };\n\n  const nodes = flattenSteps(children);\n  const total = nodes.filter((node) => isValidElement(node) && node.type === StepsItem).length;\n  const slots = stepsVariants({ orientation });\n\n  /*\n   * Positions are counted here rather than registered by each item on mount,\n   * so the count is right on the first frame: a connector that appears only\n   * after the items have reported in would flicker on every mount, and the\n   * announcement would say \"of 1\" until it did. A Provider adds no element,\n   * so wrapping an item in one leaves the layout exactly as it was.\n   */\n  let position = -1;\n  const placed = nodes.map((node, at) => {\n    if (!isValidElement(node) || node.type !== StepsItem) return node;\n    position += 1;\n    const placement = { position, total };\n    return (\n      <StepPlacementContext.Provider key={node.key ?? `step-${position}-${at}`} value={placement}>\n        {node}\n      </StepPlacementContext.Provider>\n    );\n  });\n\n  return (\n    <StepsContext.Provider value={{ activeStep, setActiveStep, orientation, separators }}>\n      <div\n        data-slot=\"steps\"\n        data-orientation={orientation}\n        data-separators={separators ? '' : undefined}\n        className={slots.root({ className })}\n        {...props}\n      >\n        {placed}\n      </div>\n    </StepsContext.Provider>\n  );\n}\nStepsRoot.displayName = 'Steps';\n\nexport interface StepsItemProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** This step's position in the flow, as you number it. */\n  step: number;\n  /** Force it complete, whatever the active step is. */\n  completed?: boolean;\n  /** Keep it from being chosen, and quiet its trigger. */\n  disabled?: boolean;\n  /** Show a spinner in the indicator while this is the active step. */\n  loading?: boolean;\n  /**\n   * Let this step be chosen while it is still ahead of the active one. For a\n   * flow that can be entered at any point; without it only steps already\n   * reached are pressable.\n   */\n  reachable?: boolean;\n  /** Classes for the box holding the trigger and its connector. */\n  className?: string;\n  /** The trigger, and a separator of your own if you want to dress this one. */\n  children?: ReactNode;\n}\n\n/**\n * One stop. It works out its own state from `step` and the root's active\n * step, and draws the connector to the next item unless it is the last one or\n * holds a separator of its own.\n */\nfunction StepsItem({\n  step,\n  completed = false,\n  disabled = false,\n  loading = false,\n  reachable = false,\n  className,\n  children,\n  ...props\n}: StepsItemProps) {\n  const { activeStep, orientation, separators } = useSteps('Steps.Item');\n  const placement = useContext(StepPlacementContext);\n\n  const { state, filled, loading: busy } = deriveStepState({\n    step,\n    activeStep,\n    completed,\n    loading,\n    disabled,\n    reachable,\n  });\n\n  /*\n   * The last item has nothing to connect to, and one placed by hand is the\n   * author's — adding a second beside it would double the line rather than\n   * replace it. Only the top level is inspected, which is where a connector\n   * has to be anyway: it is a sibling of the trigger, not something buried\n   * inside it.\n   */\n  const isLast = placement ? placement.position === placement.total - 1 : true;\n  const hasOwnSeparator = flattenSteps(children).some(\n    (child) => isValidElement(child) && child.type === StepsSeparator\n  );\n  const slots = stepsVariants({ orientation, state });\n\n  return (\n    <StepItemContext.Provider\n      value={{ step, state, filled, loading: busy, disabled, reachable, completed }}\n    >\n      <div\n        data-slot=\"steps-item\"\n        data-state={state}\n        data-position={placement?.position}\n        data-last={isLast ? '' : undefined}\n        className={slots.item({ className })}\n        {...props}\n      >\n        {children}\n        {separators && !isLast && !hasOwnSeparator ? <StepsSeparator /> : null}\n      </div>\n    </StepItemContext.Provider>\n  );\n}\nStepsItem.displayName = 'Steps.Item';\n\nexport interface StepsTriggerProps extends Omit<ComponentProps<'button'>, 'className'> {\n  /** Classes for the button, or the element `render` draws. */\n  className?: string;\n  /**\n   * Draw the trigger as another element, or compose it with a component:\n   * `render={<a href=\"/step-2\" />}`. The step's classes, handlers and\n   * announcement are put on it.\n   */\n  render?: useRender.ComponentProps<'button'>['render'];\n}\n\n/** The circle. A number, a tick once completed, or a spinner while loading. */\nexport interface StepsIndicatorProps extends Omit<ComponentProps<'span'>, 'className' | 'children'> {\n  /** Classes for the circle. */\n  className?: string;\n  /** Replaces the number, tick or spinner with something of your own. */\n  children?: ReactNode;\n  /** Draw it as another element, or compose it with a component. */\n  render?: useRender.ComponentProps<'span'>['render'];\n}\n\nfunction StepsIndicator({ className, children, render, ...props }: StepsIndicatorProps) {\n  const { orientation } = useSteps('Steps.Indicator');\n  const { state, step } = useStepItem('Steps.Indicator');\n  const placement = useContext(StepPlacementContext);\n  const slots = stepsVariants({ orientation, state });\n  const shown = children ?? (\n    state === 'loading' ? (\n      <Spinner size=\"sm\" aria-hidden />\n    ) : state === 'completed' ? (\n      <CheckIcon />\n    ) : (\n      // Steps are numbered by where they sit in the row, not by the value the\n      // caller compares; `step` may skip or start anywhere.\n      <span aria-hidden>{placement ? placement.position + 1 : step + 1}</span>\n    )\n  );\n\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'steps-indicator',\n        'data-state': state,\n        className: slots.indicator({ className }),\n        children: shown,\n      },\n      props\n    ),\n  });\n}\nStepsIndicator.displayName = 'Steps.Indicator';\n\nfunction CheckIcon() {\n  return (\n    <svg\n      aria-hidden\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"size-4\"\n    >\n      <path d=\"m3.4 8.4 3 3 6.2-6.8\" />\n    </svg>\n  );\n}\n\nexport interface StepsTitleProps extends Omit<ComponentProps<'span'>, 'className'> {\n  /** Classes for the step's name. */\n  className?: string;\n  /** Draw it as another element, such as a heading: `render={<h3 />}`. */\n  render?: useRender.ComponentProps<'span'>['render'];\n}\n\n/** What the step is called. */\nfunction StepsTitle({ className, render, ...props }: StepsTitleProps) {\n  const { orientation } = useSteps('Steps.Title');\n  const { state } = useStepItem('Steps.Title');\n  const slots = stepsVariants({ orientation, state });\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'steps-title', className: slots.title({ className }) },\n      props\n    ),\n  });\n}\nStepsTitle.displayName = 'Steps.Title';\n\nexport interface StepsDescriptionProps extends Omit<ComponentProps<'span'>, 'className'> {\n  /** Classes for the quieter line under the name. */\n  className?: string;\n  /** Draw it as another element. */\n  render?: useRender.ComponentProps<'span'>['render'];\n}\n\n/** A quieter line about the step: what it asks for, or what happened there. */\nfunction StepsDescription({ className, render, ...props }: StepsDescriptionProps) {\n  const { orientation } = useSteps('Steps.Description');\n  const { state } = useStepItem('Steps.Description');\n  const slots = stepsVariants({ orientation, state });\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'steps-description', className: slots.description({ className }) },\n      props\n    ),\n  });\n}\nStepsDescription.displayName = 'Steps.Description';\n\nexport interface StepsSeparatorProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** Classes for the connector. */\n  className?: string;\n}\n\n/**\n * The connector between one step and the next. It fills with the primary\n * colour once the step before it is complete.\n *\n * Every item draws one automatically, so this is only worth writing to dress\n * a particular connector — an item that holds its own keeps it and gets no\n * second. It belongs inside a `Steps.Item`, so it can read that item's state.\n */\nfunction StepsSeparator({ className, ...props }: StepsSeparatorProps) {\n  const { orientation } = useSteps('Steps.Separator');\n  const { state, filled } = useStepItem('Steps.Separator');\n  const slots = stepsVariants({ orientation, state });\n  return (\n    <div\n      data-slot=\"steps-separator\"\n      data-state={state}\n      data-filled={filled ? '' : undefined}\n      aria-hidden\n      className={slots.separator({ className })}\n      {...props}\n    />\n  );\n}\nStepsSeparator.displayName = 'Steps.Separator';\n\n/**\n * Makes its item selectable. Omit it for a stepper that only reports where\n * the flow is.\n *\n * It is always a `<button>`, so the element does not change under a reader\n * when a completed step becomes the current one and focus would otherwise be\n * thrown away. A step that cannot be chosen is `aria-disabled` and\n * `tabIndex={-1}` instead of being a different tag: it is out of the tab\n * order, but a reader already on it keeps their place. Pass `render` to make\n * it a link instead.\n */\nfunction StepsTrigger({\n  className,\n  children,\n  type = 'button',\n  disabled: disabledProp,\n  onClick,\n  render,\n  ...props\n}: StepsTriggerProps) {\n  const { activeStep, setActiveStep, orientation } = useSteps('Steps.Trigger');\n  const item = useStepItem('Steps.Trigger');\n  const placement = useContext(StepPlacementContext);\n  const disabled = item.disabled || disabledProp === true;\n  const { state, clickable } = deriveStepState({\n    step: item.step,\n    activeStep,\n    completed: item.completed,\n    loading: item.loading,\n    disabled,\n    reachable: item.reachable,\n  });\n  const slots = stepsVariants({ orientation, state, clickable, disabled });\n  // Loading is still the step the flow is on, so it is marked as current too.\n  const current = state === 'current' || state === 'loading';\n  const unavailable = disabled || !clickable;\n\n  const announcement = stepAnnouncement(\n    placement?.position ?? -1,\n    placement?.total ?? 0,\n    state\n  );\n\n  return useRender({\n    defaultTagName: 'button',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'steps-trigger',\n        'data-state': state,\n        'data-clickable': clickable ? '' : undefined,\n        'data-disabled': disabled ? '' : undefined,\n        'aria-current': current ? 'step' : undefined,\n        'aria-disabled': unavailable ? true : undefined,\n        'aria-busy': state === 'loading' ? true : undefined,\n        // Only a rendered tag gets `type`; a link keeps its own.\n        ...(render === undefined ? { type, tabIndex: unavailable ? -1 : undefined } : { tabIndex: unavailable ? -1 : undefined }),\n        className: slots.trigger({ className }),\n        onClick: (event: ReactMouseEvent<HTMLElement>) => {\n          if (unavailable) {\n            event.preventDefault();\n            return;\n          }\n          onClick?.(event as ReactMouseEvent<HTMLButtonElement>);\n          setActiveStep(item.step);\n        },\n        children: (\n          <>\n            {children}\n            {announcement ? <span className=\"sr-only\">, {announcement}</span> : null}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nStepsTrigger.displayName = 'Steps.Trigger';\n\nexport const Steps = Object.assign(StepsRoot, {\n  Item: StepsItem,\n  Trigger: StepsTrigger,\n  Indicator: StepsIndicator,\n  Title: StepsTitle,\n  Description: StepsDescription,\n  Separator: StepsSeparator,\n});\n\nexport {\n  StepsItem,\n  StepsTrigger,\n  StepsIndicator,\n  StepsTitle,\n  StepsDescription,\n  StepsSeparator,\n  stepsVariants,\n  deriveStepState,\n  stepAnnouncement,\n};\n"
    },
    {
      "path": "components/panelui/steps/steps-state.ts",
      "type": "registry:component",
      "target": "@components/panelui/steps/steps-state.ts",
      "content": "/**\n * What a step is, worked out from where the flow is.\n *\n * Every part of a step draws itself from this one answer. The circle, the\n * title, the connector after it and whether the trigger can be pressed are\n * not four decisions made in four places — they are four readings of one\n * state, so a step cannot be filled with a tick and still be counted as\n * still to come, and two steps cannot both call themselves current.\n *\n * Nothing here touches React or the DOM: the whole of the rule is a step's\n * number against the root's active one, which makes it the one part of the\n * component a test can pin down without rendering anything.\n */\n\n/** Where a step sits in the flow, relative to the active one. */\nexport type StepState = 'completed' | 'current' | 'upcoming' | 'loading';\n\n/** Which way the row of steps runs. */\nexport type StepsOrientation = 'horizontal' | 'vertical';\n\n/** Everything the state of one step is decided from. */\nexport interface StepStateInput {\n  /** This step's position in the flow, as the author numbers it. */\n  step: number;\n  /** The root's active step. */\n  activeStep: number;\n  /** Force the completed state, even ahead of the active step. */\n  completed?: boolean;\n  /** Show a spinner instead of the number, while this is the active step. */\n  loading?: boolean;\n  /** Keep this step from being chosen. */\n  disabled?: boolean;\n  /**\n   * Let this step be chosen while it is still upcoming. A flow that is meant\n   * to be taken one stop at a time leaves this out, and only steps already\n   * reached can be gone back to.\n   */\n  reachable?: boolean;\n}\n\n/** What every part of a step needs to know about it. */\nexport interface StepStateResult {\n  /** Which of the four states it is in. */\n  state: StepState;\n  /**\n   * Whether the connector that leaves this step is filled. Only a completed\n   * step fills it, so the line runs solid up to the active step and stops\n   * there — the segment waiting for the step ahead stays muted.\n   */\n  filled: boolean;\n  /** Whether the step can be chosen: a completed one, or any allowed ahead of time. */\n  clickable: boolean;\n  /** Whether the indicator should show a spinner rather than its number. */\n  loading: boolean;\n}\n\n/**\n * Resolves one step against the active one.\n *\n * A step before the active one is complete, the active one is current — or\n * loading, if it says so — and anything after it is upcoming. `completed`\n * wins over all of it, because a flow may know a later stage was finished\n * out of order and the circle should say so; the active step still reads as\n * current, since only one step is ever the one you are on.\n */\nexport function deriveStepState({\n  step,\n  activeStep,\n  completed = false,\n  loading = false,\n  disabled = false,\n  reachable = false,\n}: StepStateInput): StepStateResult {\n  const state: StepState = completed || step < activeStep\n    ? 'completed'\n    : step === activeStep\n      ? loading\n        ? 'loading'\n        : 'current'\n      : 'upcoming';\n\n  return {\n    state,\n    filled: state === 'completed',\n    /*\n     * What can be pressed is what would move the flow. A completed step goes\n     * back to itself; the step the flow is on would only press itself, and a\n     * step still to come has not been reached — both are marked up as\n     * content rather than left in the tab order doing nothing. `reachable`\n     * opens a step ahead of the flow for steppers that allow jumping in.\n     */\n    clickable: !disabled && (state === 'completed' || reachable),\n    loading: state === 'loading',\n  };\n}\n\n/**\n * How a step's state is said out loud, after its name.\n *\n * The active step adds nothing: it already carries `aria-current=\"step\"`, and\n * \"current\" twice is a stutter. The others say which they are, so a reader\n * arriving at the middle of a wizard hears \"Payment, step 2 of 3, completed\".\n * Before the count is known — an item used where the root cannot place it —\n * the position is left out rather than guessed at.\n */\nexport function stepAnnouncement(position: number, total: number, state: StepState): string {\n  const word =\n    state === 'completed' ? 'completed' : state === 'loading' ? 'in progress' : state === 'upcoming' ? 'not started' : null;\n  if (total < 1 || position < 0) return word ?? '';\n  const at = `step ${position + 1} of ${total}`;\n  return word ? `${at}, ${word}` : at;\n}\n"
    }
  ]
}
