{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "steps-shadcn",
  "title": "Steps (shadcn-native)",
  "author": "Lloyd Humphreys",
  "description": "EXPERIMENTAL — the API is still settling and will change in breaking ways; pin what you install. The wizard step indicator composed shadcn-natively: Tailwind theme tokens for every color, lucide icons for the completed/error/locked markers, cn, per-step icon components. Same model as steps — a headless useSteps hook the rest of the app subscribes to, earned forward progress with jump-back to reached steps, completed/error/disabled states, horizontal and vertical orientations, and a Tailwind v4 container-query collapse to a 'Step 2 of 4 — Payment' summary line in narrow containers. Self-contained in one file.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/steps/steps-shadcn.tsx",
      "content": "// steps-shadcn — the 1-2-3-4 wizard step indicator composed shadcn-natively.\n//\n// EXPERIMENTAL — the API is still settling and will change in breaking ways; pin what you\n// install.\n//\n// Same model as the vanilla `steps` (a headless engine tracking the active index and the\n// furthest-reached frontier; a row or column of markers + titles where only the active\n// step reveals its description), but built from your app's actual pieces: Tailwind theme\n// tokens for every color, `cn` for classes, lucide icons for the built-in completed /\n// error / locked markers. Inside a shadcn app it matches your theme untouched.\n//\n//   const wizard = useSteps({ steps })   headless — gate your own Continue on wizard.canNext\n//   <Steps engine={wizard.engine} />     the indicator, sharing that engine\n//   <Steps steps={steps} />              or self-managed, if you only need the indicator\n//\n// Navigation rules live in the engine: next() is the only call that grows the frontier,\n// reached steps stay clickable to jump back, forward jumps beyond the frontier are\n// blocked, disabled steps are skipped. Status is derived, never stored: disabled → error →\n// active → completed → upcoming.\n//\n// The horizontal variant is its own Tailwind v4 container (@container/steps): under 560px\n// of available width the per-step titles hide and the active step's title + description\n// take over, centered beneath the markers (which stay as a compact row). No \"Step 2 of 4\"\n// counter there: the marker row above already visualizes exactly that.\n//\n// Self-contained on purpose: the engine is inlined rather than imported from the vanilla\n// core, so this file installs alone. See steps.ts for the annotated reference\n// implementation — the navigation semantics here are identical.\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 type { ComponentProps, ComponentType, KeyboardEvent as ReactKeyboardEvent, ReactNode } from 'react'\nimport { CheckIcon, LockIcon, TriangleAlertIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\n// ── Engine ─────────────────────────────────────────────────────────────────────────────\n\ntype StepStatus = 'upcoming' | 'active' | 'completed' | 'error' | 'disabled'\ntype StepsChangeReason = 'next' | 'prev' | 'goto' | 'reset'\n\ninterface StepItem {\n  /** Stable identity for goTo/setStepError-by-id. Defaults to String(index) if omitted —\n   *  supply real ids if you'll ever splice or reorder 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. Takes precedence over the built-in\n   *  status icons. The component instantiates and sizes it — pass the component itself,\n   *  not an element. */\n  icon?: ComponentType<{ className?: string }>\n  /** Hard-disable: never reachable; next()/prev() skip over it, goTo() refuses it. */\n  disabled?: boolean\n}\n\ninterface StepsEngineOptions {\n  steps: StepItem[]\n  /** Initial active index — an uncontrolled starting point, not a controlled value. */\n  index?: number\n  /** Seed the furthest-reached frontier ahead of `index` (resuming a wizard). */\n  initialFurthest?: number\n  onChange?: (index: number, prevIndex: number, reason: StepsChangeReason) => void\n}\n\ninterface StepsEngineState {\n  index: number\n  id: string\n  count: number\n  /** Highest index ever reached via next(). Only next() grows this. */\n  furthest: number\n  steps: StepItem[]\n  status: StepStatus[]\n  isFirst: boolean\n  isLast: boolean\n  canNext: boolean\n  canPrev: boolean\n}\n\ninterface StepsEngine {\n  getState(): StepsEngineState\n  subscribe(fn: (state: StepsEngineState) => void): () => void\n  next(): void\n  prev(): void\n  goTo(target: number | string): void\n  canGoTo(target: number | string): boolean\n  indexOf(id: string): number\n  setStepError(target: number | string, error: boolean): void\n  reset(): void\n  setOptions(patch: Partial<Pick<StepsEngineOptions, 'steps'>>): void\n  destroy(): void\n}\n\nconst clampIndex = (i: number, count: number) => Math.min(Math.max(Math.floor(i), 0), count - 1)\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\nfunction createEngine(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  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  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  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  const notify = () => {\n    const s = getState()\n    subs.forEach((fn) => fn(s))\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  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  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    // Keys present in the patch are applied (`steps: undefined` is kept — it has no\n    // default); absent keys are untouched, so the component passes every prop each sync.\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\n// ── Hook ───────────────────────────────────────────────────────────────────────────────\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/** Headless: an engine plus its live state. Share `engine` with <Steps>, key your panels\n *  off `index`/`id`, and gate your own Continue button on `canNext`/`isLast`. */\nfunction useSteps(opts: UseStepsOptions): UseStepsReturn {\n  const cb = useRef({ onChange: opts.onChange })\n  cb.current = { onChange: opts.onChange }\n\n  const [engine] = useState(() =>\n    createEngine({\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\n// ── Indicator ──────────────────────────────────────────────────────────────────────────\n\ninterface 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. */\n  step?: (index: number, count: number, step: StepItem, status: StepStatus) => string\n}\n\n/** Geometry presets (px): marker diameter, marker↔text gap, title/description font sizes,\n *  connector thickness. Every color comes from theme tokens instead. */\nconst SIZES = {\n  sm: { marker: 22, gap: 8, title: 12.5, desc: 12, conn: 2 },\n  md: { marker: 28, gap: 10, title: 13.5, desc: 13, conn: 2 },\n  lg: { marker: 34, gap: 12, title: 15, desc: 14, conn: 2.5 },\n} as const\n\nconst GLIDE = 'cubic-bezier(0.22,1,0.36,1)'\n/** Breathing gap between a connector's end and the marker it meets, px. */\nconst CGAP = 3\n/** Step button padding, px — part of the connector offset math. */\nconst PAD = 4\n\ninterface StepsProps extends Omit<ComponentProps<'nav'>, 'onChange'> {\n  /** Share the engine from useSteps. Ownership is fixed at mount — supply it from the\n   *  first render. When set, the engine props below are ignored; the hook owns them. */\n  engine?: StepsEngine\n  /** Engine options, for the self-managed case (no `engine` prop). */\n  steps?: StepItem[]\n  index?: number\n  initialFurthest?: number\n  onChange?: (index: number, prevIndex: number, reason: StepsChangeReason) => void\n  /** Layout direction. Default 'horizontal'. Only horizontal collapses in narrow\n   *  containers; vertical is already compact. */\n  orientation?: 'horizontal' | 'vertical'\n  size?: 'sm' | 'md' | 'lg'\n  labels?: StepsLabels\n}\n\n/** The indicator: markers, connectors, titles, the active step's description, and the\n *  collapsed summary line. Indicator only — render your own panels off the hook's state. */\nfunction Steps({\n  engine: engineProp,\n  steps: stepsProp,\n  index: initialIndex,\n  initialFurthest,\n  onChange,\n  orientation = 'horizontal',\n  size = 'md',\n  labels,\n  className,\n  ...props\n}: StepsProps) {\n  // Engine ownership is frozen at mount: with an external engine, none is created here.\n  const ownsEngine = useRef(engineProp == null).current\n  const cb = useRef({ onChange })\n  cb.current = { onChange }\n  const [own] = useState(() =>\n    ownsEngine\n      ? createEngine({\n          steps: stepsProp ?? [],\n          index: initialIndex,\n          initialFurthest,\n          onChange: (i, p, r) => cb.current.onChange?.(i, p, r),\n        })\n      : null,\n  )\n  const engine = (engineProp ?? own) as StepsEngine\n  const [state, setState] = useState<StepsEngineState>(() => engine.getState())\n  useEffect(() => {\n    setState(engine.getState())\n    const unsubscribe = engine.subscribe(setState)\n    return () => {\n      unsubscribe()\n      if (ownsEngine) engine.destroy()\n    }\n  }, [engine, ownsEngine])\n  useEffect(() => {\n    if (ownsEngine && stepsProp) engine.setOptions({ steps: stepsProp })\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [engine, ownsEngine, stepsProp])\n\n  const horizontal = orientation !== 'vertical'\n  const g = SIZES[size]\n  const { steps, status, index, furthest, count } = state\n  const active = steps[index]\n\n  const buttonRefs = useRef<(HTMLButtonElement | null)[]>([])\n\n  const stepLabel = (i: number): string => {\n    const custom = labels?.step?.(i, count, steps[i], status[i])\n    if (custom != null) return custom\n    let label = `Step ${i + 1} of ${count}: ${steps[i].title}`\n    if (i === index && steps[i].description) label += ` — ${steps[i].description}`\n    if (status[i] === 'completed') label += ', completed'\n    else if (status[i] === 'error') label += ', has an error'\n    else if (status[i] === 'disabled') label += ', unavailable'\n    return label\n  }\n\n  const statusIcon = (s: StepStatus): ReactNode => {\n    if (s === 'completed') return <CheckIcon strokeWidth={2.6} aria-hidden=\"true\" />\n    if (s === 'error') return <TriangleAlertIcon strokeWidth={2.4} aria-hidden=\"true\" />\n    if (s === 'disabled') return <LockIcon strokeWidth={2.4} aria-hidden=\"true\" />\n    return null\n  }\n\n  // Arrow/Home/End move focus between reachable steps as a convenience on top of the\n  // standard Tab order (no roving tabindex — this is not a composite widget). Only this\n  // handler ever calls focus(); app-driven next()/goTo() can't yank focus in here.\n  const onKeyDown = (e: ReactKeyboardEvent) => {\n    const hit = (e.target as Element | null)?.closest('button')\n    if (!hit) return\n    const from = buttonRefs.current.indexOf(hit as HTMLButtonElement)\n    if (from === -1) return\n    const enabled = buttonRefs.current.map((b, i) => (b && !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) buttonRefs.current[to]?.focus()\n  }\n\n  return (\n    <nav\n      data-slot=\"steps\"\n      data-orientation={horizontal ? 'horizontal' : 'vertical'}\n      data-size={size}\n      aria-label={labels?.root ?? 'Progress'}\n      className={cn('block', horizontal && '@container/steps', className)}\n      {...props}\n    >\n      <ol\n        data-slot=\"steps-list\"\n        role=\"list\"\n        className={cn('m-0 flex list-none p-0', !horizontal && 'flex-col')}\n        onKeyDown={onKeyDown}\n      >\n        {steps.map((step, i) => {\n          const s = status[i]\n          const isActive = i === index\n          const reachable = engine.canGoTo(i)\n          const Icon = step.icon\n          return (\n            <li\n              data-slot=\"steps-item\"\n              key={step.id ?? i}\n              data-status={s}\n              data-filled={i <= furthest ? 'true' : undefined}\n              aria-current={isActive ? 'step' : undefined}\n              className={cn(\n                'group/item relative',\n                horizontal ? 'min-w-0 flex-1' : 'flex-none',\n                !horizontal && i < count - 1 && 'pb-3.5',\n              )}\n            >\n              {/* Incoming connector (horizontal): previous marker's edge to this one's. */}\n              {horizontal && i > 0 && (\n                <span\n                  data-slot=\"steps-connector\"\n                  aria-hidden=\"true\"\n                  className={cn(\n                    'absolute rounded-full transition-colors motion-reduce:transition-none',\n                    i <= furthest ? 'bg-primary' : 'bg-border',\n                  )}\n                  style={{\n                    top: PAD + g.marker / 2 - g.conn / 2,\n                    left: `calc(-50% + ${g.marker / 2 + CGAP}px)`,\n                    width: `calc(100% - ${g.marker + 2 * CGAP}px)`,\n                    height: g.conn,\n                  }}\n                />\n              )}\n              {/* Outgoing tail (vertical): below the marker, alongside the text, down to the\n                  next row — it stretches with the revealed description automatically. */}\n              {!horizontal && i < count - 1 && (\n                <span\n                  data-slot=\"steps-connector\"\n                  aria-hidden=\"true\"\n                  className={cn(\n                    'absolute rounded-full transition-colors motion-reduce:transition-none',\n                    i + 1 <= furthest ? 'bg-primary' : 'bg-border',\n                  )}\n                  style={{\n                    left: PAD + g.marker / 2 - g.conn / 2,\n                    top: PAD + g.marker + CGAP,\n                    bottom: CGAP,\n                    width: g.conn,\n                  }}\n                />\n              )}\n              <button\n                data-slot=\"steps-trigger\"\n                ref={(el) => {\n                  buttonRefs.current[i] = el\n                }}\n                type=\"button\"\n                // Native disabled gives click-blocking and unfocusability for free on\n                // unreached and disabled steps; the active step stays enabled (its goTo is\n                // a harmless no-op).\n                disabled={!reachable}\n                aria-label={stepLabel(i)}\n                onClick={() => engine.goTo(i)}\n                className={cn(\n                  'group flex w-full cursor-pointer border-0 bg-transparent p-0 disabled:cursor-default',\n                  'focus-visible:outline-none',\n                  horizontal ? 'flex-col items-center gap-1.5 text-center' : 'flex-row items-start text-start',\n                )}\n                style={{ padding: horizontal ? `${PAD}px ${g.gap}px` : PAD, gap: g.gap }}\n              >\n                <span\n                  data-slot=\"steps-marker\"\n                  className={cn(\n                    'relative z-[1] grid flex-none place-items-center rounded-full font-semibold tabular-nums',\n                    'transition-colors motion-reduce:transition-none',\n                    // C12 (deliberate exception): a soft 3px box-shadow ring reads smeared on\n                    // a circle this small — a crisp hard-edged offset outline stays legible.\n                    'group-focus-visible:outline group-focus-visible:outline-2 group-focus-visible:outline-offset-2 group-focus-visible:outline-ring',\n                    reachable && 'group-hover:brightness-[0.96]',\n                    \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[55%]\",\n                    // C14 (deliberate exception, same logic as workflow-button's terminal\n                    // stage): a locked/upcoming step reads as \"not yet\" via status color\n                    // rather than the canonical disabled:opacity-50 — keyed off the item's\n                    // data-status through the named group, not a JS lookup.\n                    'group-data-[status=upcoming]/item:bg-muted group-data-[status=upcoming]/item:text-muted-foreground',\n                    'group-data-[status=active]/item:bg-primary group-data-[status=active]/item:text-primary-foreground',\n                    'group-data-[status=completed]/item:bg-primary group-data-[status=completed]/item:text-primary-foreground',\n                    'group-data-[status=error]/item:bg-destructive group-data-[status=error]/item:text-destructive-foreground',\n                    'group-data-[status=disabled]/item:bg-muted group-data-[status=disabled]/item:text-muted-foreground group-data-[status=disabled]/item:opacity-45',\n                  )}\n                  style={{ width: g.marker, height: g.marker, fontSize: g.marker * 0.42 }}\n                >\n                  {Icon ? <Icon /> : (statusIcon(s) ?? i + 1)}\n                </span>\n                <span\n                  data-slot=\"steps-text\"\n                  className={cn(\n                    'flex min-w-0 max-w-full flex-col',\n                    horizontal ? 'items-center @max-[560px]/steps:hidden' : 'flex-1 items-start',\n                  )}\n                  style={!horizontal ? { paddingTop: Math.max(0, (g.marker - g.title * 1.4) / 2) } : undefined}\n                >\n                  <span\n                    data-slot=\"steps-title\"\n                    title={step.title}\n                    className={cn(\n                      'leading-[1.4] text-foreground transition-[color,opacity] motion-reduce:transition-none',\n                      isActive ? 'font-semibold' : 'font-medium',\n                      // C14 (see the marker above): status-driven dimming via data-status.\n                      'group-data-[status=upcoming]/item:opacity-55 group-data-[status=disabled]/item:opacity-55',\n                      'group-data-[status=error]/item:text-destructive',\n                      // Inactive titles hold one ellipsized line; the active one may wrap.\n                      horizontal && !isActive && 'max-w-full truncate',\n                    )}\n                    style={{ fontSize: g.title }}\n                  >\n                    {step.title}\n                  </span>\n                  {/* grid-rows 0fr -> 1fr animates height-to-auto with no measuring. */}\n                  <span\n                    data-slot=\"steps-description-wrap\"\n                    className={cn(\n                      'grid transition-[grid-template-rows] duration-[220ms] motion-reduce:transition-none',\n                      isActive ? '[grid-template-rows:1fr]' : '[grid-template-rows:0fr]',\n                    )}\n                    style={{ transitionTimingFunction: GLIDE }}\n                  >\n                    <span\n                      data-slot=\"steps-description\"\n                      className={cn(\n                        'min-h-0 overflow-hidden pt-[3px] leading-[1.45] text-muted-foreground',\n                        'transition-opacity delay-[60ms] motion-reduce:transition-none',\n                        isActive ? 'opacity-100' : 'opacity-0',\n                      )}\n                      style={{ fontSize: g.desc }}\n                    >\n                      {step.description}\n                    </span>\n                  </span>\n                </span>\n              </button>\n            </li>\n          )\n        })}\n      </ol>\n      {/* Collapsed summary (horizontal only): the active title + description take over\n          from the per-step titles when the container is narrow, centered. Markers stay\n          visible and interactive above it — they already visualize the position, so\n          there's no counter here. */}\n      {horizontal && (\n        <div data-slot=\"steps-summary\" className=\"mt-2.5 hidden text-center @max-[560px]/steps:block\">\n          <span data-slot=\"steps-summary-title\" className=\"font-semibold text-foreground\" style={{ fontSize: g.title }}>\n            {active.title}\n          </span>\n          {active.description ? (\n            <p\n              data-slot=\"steps-summary-description\"\n              className=\"mt-[3px] leading-[1.45] text-muted-foreground\"\n              style={{ fontSize: g.desc }}\n            >\n              {active.description}\n            </p>\n          ) : null}\n        </div>\n      )}\n    </nav>\n  )\n}\n\nexport {\n  type StepStatus,\n  type StepsChangeReason,\n  type StepItem,\n  type StepsEngineOptions,\n  type StepsEngineState,\n  type StepsEngine,\n  type UseStepsOptions,\n  type UseStepsReturn,\n  useSteps,\n  type StepsLabels,\n  type StepsProps,\n  Steps,\n}\n",
      "type": "registry:component",
      "target": "components/steps/steps-shadcn.tsx"
    }
  ],
  "type": "registry:component"
}