{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "steps",
  "title": "Steps",
  "author": "Lloyd Humphreys",
  "description": "EXPERIMENTAL — the API is still settling and will change in breaking ways; pin what you install. A dependency-free 1-2-3-4 step indicator for wizards and multi-step flows: numbered circular markers joined by connector lines, per-step titles, and a description revealed only on the active step. Navigation is earned — next() drives forward progress, reached steps stay clickable to jump back, forward jumps beyond the furthest-reached step are blocked, disabled steps are skipped. Ships a headless engine with subscribe() so the rest of the app renders its own panels off the same state, per-step icons, completed/error/disabled states, horizontal and vertical orientations, and a container-query collapse: in narrow containers the horizontal variant keeps its markers and swaps titles for a 'Step 2 of 4 — Payment' summary line. Styled on shadcn theme tokens with light-dark() fallbacks. Ships a framework-agnostic vanilla core plus a React wrapper with a useSteps hook.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/steps/steps.ts",
      "content": "// steps — a zero-dependency 1-2-3-4 step indicator for wizards and multi-step flows.\n//\n// EXPERIMENTAL — the API is still settling and will change in breaking ways; pin what you\n// install.\n//\n// A row (or column) of numbered circular markers joined by connector lines, one per step;\n// each step has a title, an optional description (revealed only while that step is active),\n// and an optional icon that replaces its number. Forward progress is earned through next();\n// any step you've already reached stays clickable to jump back, and forward jumps beyond\n// the furthest-reached step are blocked. In a narrow container the horizontal variant\n// collapses: markers stay as a compact row and the active step's title + description take\n// over from the per-step titles, centered beneath the markers (pure CSS container query —\n// no JS resize observing). No \"Step 2 of 4\" counter there: the marker row above already\n// visualizes exactly that.\n//\n// Two layers, so you choose how much it owns:\n//   createStepsEngine()   headless index + furthest-reached frontier + per-step status — no DOM\n//   createSteps()         the indicator, driving or sharing an engine\n//\n// The engine is the single source of truth and this component is deliberately *only* the\n// indicator: subscribe() (or the React useSteps hook in steps-react.tsx) is how the rest of\n// the app renders its panels, gates its own Continue button, and reacts to jumps. Status is\n// derived, never stored: disabled → error (explicit flag) → active → completed (reached but\n// not current) → upcoming. Only next() grows the frontier — browsing back to review a step\n// never un-completes anything.\n//\n// Framework-agnostic vanilla DOM — no dependencies, no build step. A React wrapper\n// (<Steps> + useSteps) lives in steps-react.tsx; a shadcn-native rebuild lives in\n// steps-shadcn.tsx.\n//\n// Not mirrored for RTL in this version: the connector math is physical (left/width), not\n// logical.\n//\n// ── State ownership ───────────────────────────────────────────────────────────────────\n// The engine and its earned-progress frontier — not a prop — are the single source of\n// truth for navigation. `index` is only an uncontrolled starting seed; there's no\n// controlled-index mode, because accepting an arbitrary index every render could silently\n// break the frontier invariant. Drive jumps through goTo()/next()/prev() instead.\n//\n// ── Theming ────────────────────────────────────────────────────────────────────────────\n// Styles consume shadcn theme tokens when present, with light-dark() fallbacks so the\n// control reads correctly standalone in both themes. Override independently of the app\n// theme via the --steps-* escape hatches (set on the root or any ancestor):\n//   --steps-marker-bg / --steps-marker-fg          upcoming marker      (default: --muted / --muted-foreground)\n//   --steps-marker-active-bg / --steps-marker-active-fg\n//                                                  active marker        (default: --primary / --primary-foreground)\n//   --steps-marker-done-bg / --steps-marker-done-fg\n//                                                  completed marker     (default: the active pair)\n//   --steps-error / --steps-error-fg               error marker + title (default: --destructive)\n//   --steps-connector                              unfilled connector   (default: --border)\n//   --steps-connector-fill                         reached connector    (default: --primary)\n//   --steps-title / --steps-title-active           title text           (default: --foreground)\n//   --steps-description                            description text     (default: --muted-foreground)\n//   --steps-ring                                   focus ring           (default: --ring)\n//   --steps-radius                                 marker radius        (default: 999px)\n//   --steps-marker-size / --steps-gap / --steps-connector-size\n//                                                  geometry. Defaults scale with size\n//                                                  sm/md/lg; setting one overrides every\n//                                                  size preset uniformly.\n// The horizontal collapse breakpoint (560px container width) is a literal in the\n// stylesheet — container query conditions can't read custom properties. To change it, ship\n// the CSS yourself: injectStyles: false + your own edited copy of stepsStyles().\n\n// ── Engine ─────────────────────────────────────────────────────────────────────────────\n\n/** Derived per-step status, in resolution order: `disabled` (the step's own flag) beats\n *  `error` (an explicit setStepError flag) beats `active` beats `completed` (reached — at\n *  or behind the furthest frontier — but not current) beats `upcoming`. */\nexport type StepStatus = 'upcoming' | 'active' | 'completed' | 'error' | 'disabled'\n\n/** How an active-step change happened: a Continue/Back call ('next' / 'prev'), a direct\n *  jump to a reached step ('goto' — marker click or steps-array reconciliation), or\n *  reset(). */\nexport type StepsChangeReason = 'next' | 'prev' | 'goto' | 'reset'\n\nexport interface StepItem {\n  /** Stable identity for goTo/setStepError-by-id. Defaults to String(index) if omitted —\n   *  supply real ids if you'll ever splice the steps array. */\n  id?: string\n  title: string\n  /** Shown only while this step is active (and in the collapsed summary line). */\n  description?: string\n  /** Optional icon replacing the number in the marker — a factory returning a fresh node,\n   *  called on every render. Takes precedence over the built-in status icons. */\n  icon?: () => Node\n  /** Hard-disable: never reachable; next()/prev() skip over it, goTo() refuses it. */\n  disabled?: boolean\n}\n\nexport interface StepsEngineOptions {\n  steps: StepItem[]\n  /** Initial active index. Default 0; clamped into range, then nudged to the nearest\n   *  non-disabled step. This is an uncontrolled starting point — drive jumps through\n   *  next()/prev()/goTo() instead. */\n  index?: number\n  /** Seed the furthest-reached frontier ahead of `index`, for resuming a wizard whose\n   *  earlier steps are already known-complete. Clamped to >= the resolved index. Default:\n   *  the resolved index (nothing pre-completed). */\n  initialFurthest?: number\n  /** Fired whenever the active step changes. When a steps patch replaces the active id at\n   *  the same array position, `index` and `prevIndex` can be equal. */\n  onChange?: (index: number, prevIndex: number, reason: StepsChangeReason) => void\n}\n\nexport interface StepsEngineState {\n  index: number\n  /** steps[index].id, or String(index) when the step has no id. */\n  id: string\n  count: number\n  /** Highest index ever reached via next(). Only next() grows this — prev()/goTo() never\n   *  shrink it, so completed markers survive browsing backward. */\n  furthest: number\n  /** The engine's current steps array (the same reference you passed in). */\n  steps: StepItem[]\n  /** One derived entry per step. */\n  status: StepStatus[]\n  isFirst: boolean\n  isLast: boolean\n  /** A non-disabled step exists after the active one. */\n  canNext: boolean\n  /** A non-disabled step exists before the active one. */\n  canPrev: boolean\n}\n\nexport interface StepsEngine {\n  getState(): StepsEngineState\n  /** Subscribe to state changes; returns unsubscribe. Fires on index/error/steps changes. */\n  subscribe(fn: (state: StepsEngineState) => void): () => void\n  /** Advance to the next non-disabled step (skipping consecutive disabled ones); no-op if\n   *  none. The only call that grows `furthest`. */\n  next(): void\n  /** Back to the previous non-disabled step; no-op if none. Never touches `furthest`. */\n  prev(): void\n  /** Jump to a reached step (by index or id): not disabled, and at or behind `furthest`.\n   *  No-op otherwise, and a silent no-op (no onChange, no notify) when `target` is already\n   *  the active step. */\n  goTo(target: number | string): void\n  /** Whether goTo(target) would be honored. True for the active step itself. */\n  canGoTo(target: number | string): boolean\n  /** Index of the step with this id (explicit or the String(index) default), or -1. */\n  indexOf(id: string): number\n  /** Flag or clear an explicit error on a step, independent of navigation. A steps patch\n   *  keeps flags attached by stable step id. */\n  setStepError(target: number | string, error: boolean): void\n  /** Back to the constructed initial index and frontier; clears every error flag. */\n  reset(): void\n  /** Patch `steps` live. Keys present in the patch are applied and absent keys are\n   *  untouched (`steps: undefined` is kept — it has no default), so the React wrapper can\n   *  pass every prop each sync. Active/reached/error state follows stable ids; if the\n   *  active id disappears or becomes disabled, the engine moves to the nearest enabled\n   *  step ('goto'). */\n  setOptions(patch: Partial<Pick<StepsEngineOptions, 'steps'>>): void\n  /** Drop subscribers. */\n  destroy(): void\n}\n\nconst clampIndex = (i: number, count: number) => Math.min(Math.max(Math.floor(i), 0), count - 1)\n\n/**\n * Headless index + frontier + per-step status — the single source of truth behind the\n * indicator and the React useSteps hook. Construction is side-effect-free (no DOM, no\n * timers), so it's safe in a useState initializer and on the server.\n */\nexport function createStepsEngine(opts: StepsEngineOptions): StepsEngine {\n  let steps = normalizeSteps(opts.steps)\n  let count = steps.length\n  const errors = new Set<number>()\n  const subs = new Set<(s: StepsEngineState) => void>()\n  const stepId = (step: StepItem, i: number) => step.id ?? String(i)\n\n  /** Nearest non-disabled step to `i`, preferring forward; `i` itself if every step is\n   *  disabled (nothing is reachable then anyway). */\n  const nearestEnabled = (i: number): number => {\n    if (!steps[i]?.disabled) return i\n    for (let d = 1; d < count; d++) {\n      if (i + d < count && !steps[i + d].disabled) return i + d\n      if (i - d >= 0 && !steps[i - d].disabled) return i - d\n    }\n    return i\n  }\n\n  let index = nearestEnabled(clampIndex(opts.index ?? 0, count))\n  let furthest = Math.max(index, clampIndex(opts.initialFurthest ?? index, count))\n  const initialIndex = index\n  const initialId = stepId(steps[index], index)\n  const initialReachedIds = new Set(\n    steps.slice(0, furthest + 1).map((step, i) => stepId(step, i)),\n  )\n\n  const statusFor = (i: number): StepStatus => {\n    if (steps[i].disabled) return 'disabled'\n    if (errors.has(i)) return 'error'\n    if (i === index) return 'active'\n    if (i <= furthest) return 'completed'\n    return 'upcoming'\n  }\n\n  const nextEnabled = (from: number): number => {\n    for (let j = from + 1; j < count; j++) if (!steps[j].disabled) return j\n    return -1\n  }\n  const prevEnabled = (from: number): number => {\n    for (let j = from - 1; j >= 0; j--) if (!steps[j].disabled) return j\n    return -1\n  }\n\n  const getState = (): StepsEngineState => {\n    const canNext = nextEnabled(index) !== -1\n    const canPrev = prevEnabled(index) !== -1\n    return {\n      index,\n      id: stepId(steps[index], index),\n      count,\n      furthest,\n      steps,\n      status: steps.map((_, i) => statusFor(i)),\n      isFirst: !canPrev,\n      isLast: !canNext,\n      canNext,\n      canPrev,\n    }\n  }\n\n  const notify = () => {\n    const s = getState()\n    subs.forEach((fn) => fn(s))\n  }\n\n  const jump = (to: number, reason: StepsChangeReason) => {\n    const prev = index\n    index = to\n    if (index !== prev) opts.onChange?.(index, prev, reason)\n    notify()\n  }\n\n  const resolve = (target: number | string): number => {\n    if (typeof target === 'number') {\n      const i = Math.floor(target)\n      return i >= 0 && i < count ? i : -1\n    }\n    return steps.findIndex((s, i) => stepId(s, i) === target)\n  }\n\n  const canGoTo = (target: number | string): boolean => {\n    const i = resolve(target)\n    return i !== -1 && !steps[i].disabled && i <= furthest\n  }\n\n  return {\n    getState,\n    subscribe(fn) {\n      subs.add(fn)\n      return () => subs.delete(fn)\n    },\n    next() {\n      const j = nextEnabled(index)\n      if (j === -1) return\n      furthest = Math.max(furthest, j)\n      jump(j, 'next')\n    },\n    prev() {\n      const j = prevEnabled(index)\n      if (j === -1) return\n      jump(j, 'prev')\n    },\n    goTo(target) {\n      const i = resolve(target)\n      if (i === -1 || i === index || !canGoTo(i)) return\n      jump(i, 'goto')\n    },\n    canGoTo,\n    indexOf: (id) => steps.findIndex((s, i) => stepId(s, i) === id),\n    setStepError(target, error) {\n      const i = resolve(target)\n      if (i === -1) return\n      const changed = error ? (errors.has(i) ? false : (errors.add(i), true)) : errors.delete(i)\n      if (changed) notify()\n    },\n    reset() {\n      errors.clear()\n      const initialMatch = steps.findIndex((step, i) => stepId(step, i) === initialId)\n      const to = nearestEnabled(\n        initialMatch === -1 ? clampIndex(initialIndex, count) : initialMatch,\n      )\n      furthest = to\n      steps.forEach((step, i) => {\n        if (initialReachedIds.has(stepId(step, i))) furthest = Math.max(furthest, i)\n      })\n      jump(to, 'reset')\n    },\n    setOptions(patch) {\n      if ('steps' in patch && patch.steps != null) {\n        const prev = index\n        const previousId = stepId(steps[index], index)\n        const reachedIds = new Set(\n          steps.slice(0, furthest + 1).map((step, i) => stepId(step, i)),\n        )\n        const errorIds = new Set(\n          [...errors].map((i) => stepId(steps[i], i)),\n        )\n\n        steps = normalizeSteps(patch.steps)\n        count = steps.length\n        errors.clear()\n        steps.forEach((step, i) => {\n          if (errorIds.has(stepId(step, i))) errors.add(i)\n        })\n\n        const currentMatch = steps.findIndex((step, i) => stepId(step, i) === previousId)\n        index = nearestEnabled(\n          currentMatch === -1 ? clampIndex(prev, count) : currentMatch,\n        )\n        furthest = index\n        steps.forEach((step, i) => {\n          if (reachedIds.has(stepId(step, i))) furthest = Math.max(furthest, i)\n        })\n        if (stepId(steps[index], index) !== previousId) {\n          opts.onChange?.(index, prev, 'goto')\n        }\n      }\n      notify()\n    },\n    destroy() {\n      subs.clear()\n    },\n  }\n}\n\nfunction normalizeSteps(steps: StepItem[]): StepItem[] {\n  if (steps.length > 0) return steps\n  console.warn('steps: `steps` is empty — substituting a single disabled placeholder step.')\n  return [{ title: 'Step', disabled: true }]\n}\n\n// ── Indicator ──────────────────────────────────────────────────────────────────────────\n\nexport interface StepsLabels {\n  /** Accessible name of the indicator. Default 'Progress'. */\n  root?: string\n  /** Accessible name per step button. Default composes `Step ${i + 1} of ${count}: ${title}`\n   *  plus the description while active and a status suffix (completed / error / disabled). */\n  step?: (index: number, count: number, step: StepItem, status: StepStatus) => string\n}\n\nexport interface StepsOptions extends StepsEngineOptions {\n  /** Drive an engine you already own (e.g. shared with your own panels, or from the React\n   *  hook) instead of creating one. When set, every engine option on this object (steps,\n   *  index, initialFurthest, onChange) is ignored — the engine owns those — and only the\n   *  presentational options below apply. */\n  engine?: StepsEngine\n  /** Layout direction. Default 'horizontal'. Only the horizontal variant collapses in\n   *  narrow containers; vertical is already compact. */\n  orientation?: 'horizontal' | 'vertical'\n  /** Geometry preset. Default 'md'. (Every dimension is also a --steps-* variable.) */\n  size?: 'sm' | 'md' | 'lg'\n  labels?: StepsLabels\n  /** Inject the component stylesheet on first use. Default true; set false to ship the CSS\n   *  yourself (see `stepsStyles()`). */\n  injectStyles?: boolean\n  /** Extra class(es) added to the root, for your own overrides. */\n  className?: string\n}\n\nexport interface Steps {\n  /** The control root. Append it anywhere. */\n  readonly element: HTMLElement\n  /** The engine driving this indicator (own or shared) — subscribe to sync your panels. */\n  readonly engine: StepsEngine\n  getState(): StepsEngineState\n  next(): void\n  prev(): void\n  goTo(target: number | string): void\n  setStepError(target: number | string, error: boolean): void\n  reset(): void\n  /** Patch presentational options (orientation, size, labels, className) and — when the\n   *  indicator owns its engine — steps. Keys present in the patch are applied, with\n   *  `undefined` resetting that option to its default; absent keys are untouched. (The\n   *  React wrapper passes every prop each sync, so a removed prop genuinely resets.) */\n  setState(patch: Partial<StepsOptions>): void\n  /** Detach listeners; destroys the engine only if the indicator created it. */\n  destroy(): void\n}\n\n/** Build the indicator. Append `.element` anywhere; the horizontal variant is its own CSS\n *  container and collapses itself under 560px of available width. */\nexport function createSteps(opts: StepsOptions): Steps {\n  if (opts.injectStyles !== false) injectStepsStyles()\n\n  const engine = opts.engine ?? createStepsEngine(opts)\n  const ownsEngine = !opts.engine\n  let orientation = opts.orientation ?? 'horizontal'\n  let size = opts.size ?? 'md'\n  let labels = opts.labels\n  let className = opts.className\n  let steps = engine.getState().steps\n\n  const root = document.createElement('nav')\n  const list = document.createElement('ol')\n  list.className = 'steps-list'\n  // list-style: none strips the implicit list semantics in Safari/VoiceOver — restore them.\n  list.setAttribute('role', 'list')\n  const summary = document.createElement('div')\n  summary.className = 'steps-summary'\n  const summaryTitle = document.createElement('span')\n  summaryTitle.className = 'steps-summary-title'\n  const summaryDesc = document.createElement('p')\n  summaryDesc.className = 'steps-summary-desc'\n  summary.append(summaryTitle, summaryDesc)\n  root.append(list, summary)\n\n  let items: HTMLLIElement[] = []\n  let buttons: HTMLButtonElement[] = []\n  let markers: HTMLSpanElement[] = []\n\n  const applyLayout = () => {\n    root.className = `steps steps--${orientation} steps--${size}${className ? ` ${className}` : ''}`\n    root.setAttribute('aria-label', labels?.root ?? 'Progress')\n  }\n\n  const buildList = () => {\n    list.replaceChildren()\n    items = []\n    buttons = []\n    markers = []\n    steps.forEach((step, i) => {\n      const li = document.createElement('li')\n      li.className = 'step'\n      const btn = document.createElement('button')\n      btn.type = 'button'\n      btn.className = 'step-hit'\n      const marker = document.createElement('span')\n      marker.className = 'step-marker'\n      const text = document.createElement('span')\n      text.className = 'step-text'\n      const title = document.createElement('span')\n      title.className = 'step-title'\n      title.textContent = step.title\n      title.title = step.title // ellipsized inactive titles keep a native tooltip\n      const descWrap = document.createElement('span')\n      descWrap.className = 'step-description-wrap'\n      const desc = document.createElement('p')\n      desc.className = 'step-description'\n      desc.textContent = step.description ?? ''\n      descWrap.appendChild(desc)\n      text.append(title, descWrap)\n      btn.append(marker, text)\n      btn.addEventListener('click', () => engine.goTo(i))\n      li.appendChild(btn)\n      list.appendChild(li)\n      items.push(li)\n      buttons.push(btn)\n      markers.push(marker)\n    })\n  }\n\n  const stepLabel = (i: number, s: StepsEngineState): string => {\n    const step = steps[i]\n    const status = s.status[i]\n    const custom = labels?.step?.(i, s.count, step, status)\n    if (custom != null) return custom\n    let label = `Step ${i + 1} of ${s.count}: ${step.title}`\n    if (i === s.index && step.description) label += ` — ${step.description}`\n    if (status === 'completed') label += ', completed'\n    else if (status === 'error') label += ', has an error'\n    else if (status === 'disabled') label += ', unavailable'\n    return label\n  }\n\n  /** Built-in status icons — only these trusted strings ever go through innerHTML; user\n   *  text is always textContent. */\n  const statusIcon = (status: StepStatus): SVGSVGElement | null => {\n    if (status !== 'completed' && status !== 'error' && status !== 'disabled') return null\n    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')\n    svg.setAttribute('viewBox', '0 0 24 24')\n    svg.setAttribute('aria-hidden', 'true')\n    svg.setAttribute('fill', 'none')\n    svg.setAttribute('stroke', 'currentColor')\n    svg.setAttribute('stroke-width', '2.6')\n    svg.setAttribute('stroke-linecap', 'round')\n    svg.setAttribute('stroke-linejoin', 'round')\n    if (status === 'completed') svg.innerHTML = '<path d=\"m5 12.5 4.5 4.5L19 7.5\"/>'\n    else if (status === 'error') svg.innerHTML = '<path d=\"M12 9v4.5\"/><path d=\"M12 17.2v.05\"/><path d=\"M10.3 4.1 2.9 17a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.1a2 2 0 0 0-3.4 0Z\"/>'\n    else svg.innerHTML = '<rect x=\"5.5\" y=\"10.5\" width=\"13\" height=\"9.5\" rx=\"2\"/><path d=\"M8.5 10.5V7.5a3.5 3.5 0 0 1 7 0v3\"/>'\n    return svg\n  }\n\n  const render = (s: StepsEngineState) => {\n    // A shared engine can change steps out from under us (its owner's setOptions) — this\n    // subscription is the only channel that reaches the indicator, so rebuild here.\n    if (s.steps !== steps) {\n      steps = s.steps\n      buildList()\n    }\n    items.forEach((li, i) => {\n      const status = s.status[i]\n      li.dataset.status = status\n      if (i <= s.furthest) li.dataset.filled = 'true'\n      else delete li.dataset.filled\n      if (i === s.index) li.setAttribute('aria-current', 'step')\n      else li.removeAttribute('aria-current')\n      const btn = buttons[i]\n      // Native disabled gives click-blocking and unfocusability for free on unreached and\n      // disabled steps; the active step stays enabled (its goTo is a harmless no-op).\n      btn.disabled = !engine.canGoTo(i)\n      btn.setAttribute('aria-label', stepLabel(i, s))\n      markers[i].replaceChildren(steps[i].icon?.() ?? statusIcon(status) ?? document.createTextNode(String(i + 1)))\n    })\n    summaryTitle.textContent = steps[s.index].title\n    summaryDesc.textContent = steps[s.index].description ?? ''\n    summaryDesc.style.display = steps[s.index].description ? '' : 'none'\n  }\n\n  // Arrow/Home/End move focus between reachable steps as a convenience on top of the\n  // standard Tab order (this is not a composite widget — no roving tabindex). Only this\n  // handler ever calls focus(); the render loop never does, so app-driven next()/goTo()\n  // can't yank focus into the indicator.\n  const onKeyDown = (e: KeyboardEvent) => {\n    const hit = (e.target as Element | null)?.closest('.step-hit')\n    if (!hit) return\n    const from = buttons.indexOf(hit as HTMLButtonElement)\n    if (from === -1) return\n    const enabled = buttons.map((b, i) => (!b.disabled ? i : -1)).filter((i) => i !== -1)\n    let to = -1\n    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') to = enabled.find((i) => i > from) ?? -1\n    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') to = [...enabled].reverse().find((i) => i < from) ?? -1\n    else if (e.key === 'Home') to = enabled[0] ?? -1\n    else if (e.key === 'End') to = enabled[enabled.length - 1] ?? -1\n    else return\n    e.preventDefault()\n    if (to !== -1 && to !== from) buttons[to].focus()\n  }\n  list.addEventListener('keydown', onKeyDown)\n\n  applyLayout()\n  buildList()\n  render(engine.getState())\n  const unsubscribe = engine.subscribe(render)\n\n  return {\n    element: root,\n    engine,\n    getState: () => engine.getState(),\n    next: () => engine.next(),\n    prev: () => engine.prev(),\n    goTo: (t) => engine.goTo(t),\n    setStepError: (t, error) => engine.setStepError(t, error),\n    reset: () => engine.reset(),\n    setState(patch) {\n      if (ownsEngine && 'steps' in patch) engine.setOptions({ steps: patch.steps })\n      if ('orientation' in patch) orientation = patch.orientation ?? 'horizontal'\n      if ('size' in patch) size = patch.size ?? 'md'\n      if ('labels' in patch) labels = patch.labels\n      if ('className' in patch) className = patch.className\n      applyLayout()\n      render(engine.getState())\n    },\n    destroy() {\n      unsubscribe()\n      list.removeEventListener('keydown', onKeyDown)\n      if (ownsEngine) engine.destroy()\n    },\n  }\n}\n\n// ── Styles ─────────────────────────────────────────────────────────────────────────────\n\nlet stylesInjected = false\n/** Inject the indicator stylesheet once. Called automatically unless `injectStyles: false`. */\nexport function injectStepsStyles(): void {\n  if (stylesInjected || typeof document === 'undefined') return\n  if (document.getElementById('steps-styles')) {\n    stylesInjected = true\n    return\n  }\n  const style = document.createElement('style')\n  style.id = 'steps-styles'\n  style.textContent = stepsStyles()\n  document.head.appendChild(style)\n  stylesInjected = true\n}\n\n/** The indicator's CSS as a string (for callers who inject styles themselves / SSR). */\nexport function stepsStyles(): string {\n  return `\n.steps {\n  display: block;\n  /* Size presets re-reference the same override variable with different fallbacks (never\n     reassign it), so one consumer-set --steps-* wins across every size uniformly. */\n  --_marker: var(--steps-marker-size, 28px);\n  --_gap: var(--steps-gap, 10px);\n  --_conn: var(--steps-connector-size, 2px);\n  --_cgap: 3px;\n  --_pad: 4px;\n  --_title-size: 13.5px;\n  --_desc-size: 13px;\n}\n.steps--sm { --_marker: var(--steps-marker-size, 22px); --_gap: var(--steps-gap, 8px); --_title-size: 12.5px; --_desc-size: 12px; }\n.steps--lg { --_marker: var(--steps-marker-size, 34px); --_gap: var(--steps-gap, 12px); --_conn: var(--steps-connector-size, 2.5px); --_title-size: 15px; --_desc-size: 14px; }\n/* The root is its own query container, so the collapse reacts to the space the indicator\n   actually gets — no wrapper element or viewport breakpoint involved. */\n.steps--horizontal { container-type: inline-size; container-name: steps; }\n.steps-list {\n  list-style: none; margin: 0; padding: 0;\n  display: flex;\n}\n.steps--vertical .steps-list { flex-direction: column; }\n.step { position: relative; flex: 1 1 0; min-width: 0; }\n.steps--vertical .step { flex: none; }\n.steps--vertical .step:not(:last-child) { padding-bottom: 14px; }\n.step-hit {\n  appearance: none; -webkit-appearance: none; border: 0; margin: 0;\n  background: transparent; cursor: pointer; color: inherit; font: inherit;\n  display: flex; flex-direction: column; align-items: center; gap: 6px;\n  width: 100%; padding: var(--_pad) var(--_gap);\n  text-align: center;\n}\n.step-hit:disabled { cursor: default; }\n.steps--vertical .step-hit {\n  flex-direction: row; align-items: flex-start; gap: var(--_gap);\n  text-align: start; padding: var(--_pad);\n}\n.step-marker {\n  position: relative; z-index: 1;\n  display: grid; place-items: center; flex: none;\n  width: var(--_marker); height: var(--_marker);\n  border-radius: var(--steps-radius, 999px);\n  background: var(--steps-marker-bg, var(--muted, light-dark(#ececee, #26262b)));\n  color: var(--steps-marker-fg, var(--muted-foreground, light-dark(#8a8a93, #8b8b95)));\n  font-size: calc(var(--_marker) * 0.42); font-weight: 600;\n  font-variant-numeric: tabular-nums;\n  transition: background-color 0.2s ease, color 0.2s ease;\n}\n.step-marker svg { width: 55%; height: 55%; }\n.step-hit:not(:disabled):hover .step-marker { filter: brightness(0.96); }\n.step[data-status=\"active\"] .step-marker {\n  background: var(--steps-marker-active-bg, var(--primary, light-dark(#2f2f33, #e4e4e7)));\n  color: var(--steps-marker-active-fg, var(--primary-foreground, light-dark(#fafafa, #18181b)));\n}\n.step[data-status=\"completed\"] .step-marker {\n  background: var(--steps-marker-done-bg, var(--steps-marker-active-bg, var(--primary, light-dark(#2f2f33, #e4e4e7))));\n  color: var(--steps-marker-done-fg, var(--steps-marker-active-fg, var(--primary-foreground, light-dark(#fafafa, #18181b))));\n}\n.step[data-status=\"error\"] .step-marker {\n  background: var(--steps-error, var(--destructive, light-dark(#dc2626, #ef4444)));\n  color: var(--steps-error-fg, light-dark(#fafafa, #fafafa));\n}\n.step[data-status=\"disabled\"] .step-marker { opacity: 0.45; }\n/* ── Connectors ──\n   Horizontal: each column is flex: 1 1 0, so the boundary between neighbors is exact and\n   the incoming line for step i runs from the previous marker's edge to its own. */\n.steps--horizontal .step:not(:first-child)::before {\n  content: ''; position: absolute;\n  top: calc(var(--_pad) + var(--_marker) / 2 - var(--_conn) / 2);\n  left: calc(-50% + var(--_marker) / 2 + var(--_cgap));\n  width: calc(100% - var(--_marker) - 2 * var(--_cgap));\n  height: var(--_conn); border-radius: 999px;\n  background: var(--steps-connector, var(--border, light-dark(#dcdce1, #3a3a42)));\n  transition: background-color 0.2s ease;\n}\n.steps--horizontal .step[data-filled]:not(:first-child)::before {\n  background: var(--steps-connector-fill, var(--primary, light-dark(#2f2f33, #e4e4e7)));\n}\n/* Vertical: an outgoing tail below each marker, running alongside the text down to the next\n   row — it stretches with the active step's revealed description automatically. Filled when\n   the *next* step has been reached (the same segment the horizontal ::before paints). */\n.steps--vertical .step:not(:last-child)::after {\n  content: ''; position: absolute;\n  left: calc(var(--_pad) + var(--_marker) / 2 - var(--_conn) / 2);\n  top: calc(var(--_pad) + var(--_marker) + var(--_cgap));\n  bottom: var(--_cgap);\n  width: var(--_conn); border-radius: 999px;\n  background: var(--steps-connector, var(--border, light-dark(#dcdce1, #3a3a42)));\n  transition: background-color 0.2s ease;\n}\n.steps--vertical .step:has(+ .step[data-filled])::after {\n  background: var(--steps-connector-fill, var(--primary, light-dark(#2f2f33, #e4e4e7)));\n}\n.step-text { display: flex; flex-direction: column; align-items: center; min-width: 0; max-width: 100%; }\n.steps--vertical .step-text { align-items: flex-start; flex: 1 1 auto; padding-top: calc((var(--_marker) - var(--_title-size) * 1.4) / 2); }\n.step-title {\n  font-size: var(--_title-size); font-weight: 500; line-height: 1.4;\n  color: var(--steps-title, var(--foreground, light-dark(#3f3f46, #d4d4d8)));\n  transition: color 0.2s ease, opacity 0.2s ease;\n}\n.step[data-status=\"upcoming\"] .step-title, .step[data-status=\"disabled\"] .step-title { opacity: 0.55; }\n/* \"Current step\" visuals key off aria-current, not data-status — an errored active step\n   keeps its error marker/title color yet still reads as the step you're on. */\n.step[aria-current=\"step\"] .step-title {\n  font-weight: 600;\n  color: var(--steps-title-active, var(--foreground, light-dark(#18181b, #fafafa)));\n}\n.step[data-status=\"error\"] .step-title { color: var(--steps-error, var(--destructive, light-dark(#dc2626, #ef4444))); }\n/* Inactive titles hold one ellipsized line so columns stay tidy; the active title (with the\n   most visual room) is allowed to wrap. */\n.steps--horizontal .step:not([aria-current=\"step\"]) .step-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }\n/* Description reveal: grid-rows 0fr -> 1fr animates height-to-auto with no JS measuring.\n   Every step owns an (empty when inactive) wrap, so the reveal happens in place. */\n.step-description-wrap {\n  display: grid; grid-template-rows: 0fr;\n  transition: grid-template-rows 220ms cubic-bezier(0.22, 1, 0.36, 1);\n}\n.step-description-wrap > .step-description { overflow: hidden; min-height: 0; }\n.step[aria-current=\"step\"] .step-description-wrap { grid-template-rows: 1fr; }\n.step-description {\n  margin: 0; padding-top: 3px;\n  font-size: var(--_desc-size); line-height: 1.45;\n  color: var(--steps-description, var(--muted-foreground, light-dark(#8a8a93, #8b8b95)));\n  opacity: 0;\n  transition: opacity 0.2s ease 60ms;\n}\n.step[aria-current=\"step\"] .step-description { opacity: 1; }\n.step-hit:focus-visible { outline: none; }\n.step-hit:focus-visible .step-marker {\n  outline: 2px solid var(--steps-ring, var(--ring, light-dark(#a1a1aa, #71717a)));\n  outline-offset: 2px;\n}\n/* ── Collapsed summary (horizontal only) ──\n   Under 560px of container width the per-step titles hide, markers tighten into a compact\n   row, and the active title + description take over, centered. No \"Step 2 of 4\" counter:\n   the marker row above already visualizes the position. The breakpoint is a literal:\n   container query conditions can't read custom properties. */\n.steps-summary { display: none; text-align: center; }\n.steps-summary-title {\n  font-size: var(--_title-size); font-weight: 600;\n  color: var(--steps-title-active, var(--foreground, light-dark(#18181b, #fafafa)));\n}\n.steps-summary-desc {\n  margin: 3px 0 0;\n  font-size: var(--_desc-size); line-height: 1.45;\n  color: var(--steps-description, var(--muted-foreground, light-dark(#8a8a93, #8b8b95)));\n}\n@container steps (max-width: 560px) {\n  /* A container query can't style its own container, so the overrides land on .steps-list\n     (and the summary), which every marker/connector reads through inheritance. */\n  .steps--horizontal .steps-list { --_marker: var(--steps-marker-size, 22px); --_gap: var(--steps-gap, 4px); }\n  .steps--horizontal .step-text { display: none; }\n  .steps--horizontal .steps-summary { display: block; margin-top: 10px; }\n}\n@media (prefers-reduced-motion: reduce) {\n  .step::before, .step::after, .step-marker, .step-title, .step-description-wrap, .step-description { transition: none !important; }\n}\n`\n}\n",
      "type": "registry:lib",
      "target": "components/steps/steps.ts"
    },
    {
      "path": "registry/steps/steps-react.tsx",
      "content": "// steps-react — a thin React wrapper over the framework-agnostic steps core.\n//\n// EXPERIMENTAL — the API is still settling and will change in breaking ways; pin what you\n// install.\n//\n//   useSteps({ steps })                  headless: engine + live state, no DOM\n//   <Steps engine={wizard.engine} />     the indicator, driven by that engine\n//   <Steps steps={steps} />              or self-managed, if you only need the indicator\n//\n// The hook owns the engine (one frontier, one source of truth); the indicator mounts the\n// vanilla control and shares it. Key your own panels off the hook's `index`/`id` — and gate\n// your own Continue button on its state — and everything stays in sync by construction.\n//\n// State ownership: the engine and its frontier own navigation, not React state. `index` is\n// only an uncontrolled starting seed — there's deliberately no controlled-index prop, since\n// an arbitrary index on every render could silently break the earned-progress invariant.\n\n'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport {\n  createSteps,\n  createStepsEngine,\n  type StepItem,\n  type StepsChangeReason,\n  type StepsEngine,\n  type StepsEngineOptions,\n  type StepsEngineState,\n  type StepsLabels,\n  type StepStatus,\n  type Steps as VanillaSteps,\n} from './steps'\n\ninterface UseStepsOptions extends StepsEngineOptions {}\n\ninterface UseStepsReturn extends StepsEngineState {\n  /** Hand this to <Steps engine={…}> (and anything else) to share the one source of truth. */\n  engine: StepsEngine\n  next: () => void\n  prev: () => void\n  goTo: (target: number | string) => void\n  setStepError: (target: number | string, error: boolean) => void\n  reset: () => void\n}\n\n/**\n * Headless: an engine plus its live state as React state. Callbacks are read through a ref,\n * so inline closures are fine; `steps` patches into the running engine (keep the array\n * referentially stable — memoize it — or the indicator rebuilds every render).\n */\nfunction useSteps(opts: UseStepsOptions): UseStepsReturn {\n  const cb = useRef({ onChange: opts.onChange })\n  cb.current.onChange = opts.onChange\n\n  const [engine] = useState(() =>\n    createStepsEngine({\n      steps: opts.steps,\n      index: opts.index,\n      initialFurthest: opts.initialFurthest,\n      onChange: (i, p, r) => cb.current.onChange?.(i, p, r),\n    }),\n  )\n  const [state, setState] = useState<StepsEngineState>(() => engine.getState())\n\n  useEffect(() => {\n    const unsubscribe = engine.subscribe(setState)\n    return () => {\n      unsubscribe()\n      engine.destroy()\n    }\n  }, [engine])\n\n  useEffect(() => {\n    engine.setOptions({ steps: opts.steps })\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [engine, opts.steps])\n\n  return {\n    engine,\n    ...state,\n    next: engine.next,\n    prev: engine.prev,\n    goTo: (t) => engine.goTo(t),\n    setStepError: (t, error) => engine.setStepError(t, error),\n    reset: engine.reset,\n  }\n}\n\ninterface StepsProps {\n  /** Share the engine from useSteps. When set, the engine props below (steps, index,\n   *  initialFurthest, onChange) are ignored — the hook owns them. */\n  engine?: StepsEngine\n  /** Engine options, for the self-managed case (no `engine` prop). */\n  steps?: StepItem[]\n  /** Initial step (self-managed only) — an uncontrolled starting point, not a controlled\n   *  value; drive jumps through the hook/engine instead. */\n  index?: number\n  initialFurthest?: number\n  onChange?: (index: number, prevIndex: number, reason: StepsChangeReason) => void\n  /** Presentation — see StepsOptions in steps.ts for details. */\n  orientation?: 'horizontal' | 'vertical'\n  size?: 'sm' | 'md' | 'lg'\n  labels?: StepsLabels\n  className?: string\n}\n\n/**\n * The indicator. The returned wrapper is `display: contents`, so it adds no layout box of\n * its own. Re-created only when the engine identity changes; every other prop syncs into\n * the running control.\n */\nfunction Steps({\n  engine,\n  steps,\n  index,\n  initialFurthest,\n  onChange,\n  orientation,\n  size,\n  labels,\n  className,\n}: StepsProps) {\n  const hostRef = useRef<HTMLSpanElement>(null)\n  const controlRef = useRef<VanillaSteps | null>(null)\n  // Keep the latest callback without re-creating the control each render.\n  const cb = useRef({ onChange })\n  cb.current = { onChange }\n  // Initial-only engine options, captured at creation like useState initializers.\n  const initial = useRef({ steps, index, initialFurthest, orientation, size, labels, className })\n  initial.current = { steps, index, initialFurthest, orientation, size, labels, className }\n\n  useEffect(() => {\n    const host = hostRef.current\n    if (!host) return\n    const init = initial.current\n    const control = createSteps({\n      engine,\n      steps: init.steps ?? engine?.getState().steps ?? [],\n      index: init.index,\n      initialFurthest: init.initialFurthest,\n      onChange: (i, p, r) => cb.current.onChange?.(i, p, r),\n      orientation: init.orientation,\n      size: init.size,\n      labels: init.labels,\n      className: init.className,\n    })\n    host.appendChild(control.element)\n    controlRef.current = control\n    return () => {\n      control.destroy()\n      control.element.remove()\n      controlRef.current = null\n    }\n  }, [engine])\n\n  // Sync presentational (and, when self-managed, engine) options into the live control.\n  useEffect(() => {\n    controlRef.current?.setState({ steps, orientation, size, labels, className })\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [steps, orientation, size, labels, className])\n\n  return <span ref={hostRef} style={{ display: 'contents' }} />\n}\n\nexport {\n  type StepItem,\n  type StepsChangeReason,\n  type StepsEngine,\n  type StepsEngineOptions,\n  type StepsEngineState,\n  type StepsLabels,\n  type StepStatus,\n  type UseStepsOptions,\n  type UseStepsReturn,\n  useSteps,\n  type StepsProps,\n  Steps,\n}\n",
      "type": "registry:component",
      "target": "components/steps/steps-react.tsx"
    }
  ],
  "type": "registry:component"
}