{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thought-trail",
  "type": "registry:ui",
  "title": "Thought Trail",
  "description": "An agent's steps as they happen. The step at work shimmers and streams its details; when the next one starts, it folds up into a one-line summary and the rail fills down to the new step. Click any summary to open it again.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/thought-trail.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type MotionValue,\n  type Transition,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * THOUGHT TRAIL STORYBOARD\n *\n *  arrive    a new step opens in at the bottom: its row grows\n *            from nothing as it fades down into place, so the\n *            trail lengthens instead of jumping\n *  work      its title shimmers, a band of light sweeping left\n *            to right; an arc spins in its dot, and its details\n *            open underneath and grow with each line that\n *            streams in\n *  hand off  the next step starts: the last one's details fold\n *            away, its title rolls up into its summary, the arc\n *            draws into a check and the rail fills down toward\n *            the new step, which is already opening. One spring\n *            for all of it, so the height eases, never jolts\n *  revisit   click a summary to open it again. A step you\n *            opened stays open; the trail only closes the ones\n *            it opened itself\n *  fail      the dot becomes a red cross with a small shake,\n *            the rail stops there, and the step stays open\n *  finish    the header's clock stops and its label rolls to\n *            \"Worked for 14s · 5 steps\"; a beat later the whole\n *            trail folds up into that line. A failed run stays\n *            open\n *  fold      fold the trail by hand while it runs and the\n *            header keeps the current step's title, shimmering\n *  reduced   no shimmer, slide or roll: heights change at\n *            once and labels crossfade\n * ───────────────────────────────────────────────────────── */\n\nconst TRAIL = {\n  fold: { type: \"spring\", visualDuration: 0.34, bounce: 0 }, // details opening and closing, steps arriving\n  grow: { type: \"spring\", visualDuration: 0.26, bounce: 0 }, // details following what streams in\n  roll: { type: \"spring\", visualDuration: 0.3, bounce: 0.1 }, // a title rolling into its summary\n  pop: { type: \"spring\", visualDuration: 0.24, bounce: 0.2 }, // dot to arc to check\n  show: { duration: 0.22, ease: \"easeOut\", delay: 0.06 },\n  hide: { duration: 0.12, ease: \"easeIn\" },\n  draw: { duration: 0.3, ease: [0.65, 0, 0.35, 1] }, // check and cross strokes\n  rail: { duration: 0.45, ease: [0.22, 1, 0.36, 1] },\n  sheen: 1.8, // s for the band of light to cross a title\n  spin: 0.9, // s per turn of the working arc\n  settle: 0.8, // s the finished header holds before the trail folds into it\n  tick: 250, // ms between clock readings\n  drift: 6, // px details slide as they fade\n} as const satisfies Record<string, Transition | number>;\n\nexport type ThoughtTrailStatus = \"pending\" | \"active\" | \"done\" | \"error\";\n\nconst TONE: Record<ThoughtTrailStatus, string> = {\n  pending: \"text-zinc-400 dark:text-zinc-600\",\n  active: \"text-zinc-900 dark:text-zinc-100\",\n  done: \"text-zinc-500 dark:text-zinc-400\",\n  error: \"text-rose-600 dark:text-rose-400\",\n};\n\nconst DOT_TONE: Record<ThoughtTrailStatus, string> = {\n  pending: \"text-zinc-300 dark:text-zinc-700\",\n  active: \"text-zinc-900 dark:text-zinc-100\",\n  done: \"text-zinc-400 dark:text-zinc-500\",\n  error: \"text-rose-500 dark:text-rose-400\",\n};\n\n// The title's own color at 60%, with a band of the full color through the middle.\nconst DIM = \"color-mix(in oklab, currentColor 60%, transparent)\";\nconst SHEEN = `linear-gradient(90deg, ${DIM} 0%, ${DIM} 41%, currentColor 50%, ${DIM} 59%, ${DIM} 100%)`;\n\nconst FOCUS =\n  \"rounded-md outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-600\";\n\n/* ── What the steps tell the header ─────────────────────── */\n\ntype Entry = { status: ThoughtTrailStatus; title: React.ReactNode };\ntype Snapshot = {\n  /** Steps that have started. */\n  steps: number;\n  failed: boolean;\n  /** Title of the latest active step. */\n  current: React.ReactNode;\n};\n\n/**\n * Steps write here from layout effects and the header reads it, so a step's\n * update re-renders the header, never the steps around it. The trail itself\n * only listens for `failed`.\n */\nfunction createStore() {\n  const entries = new Map<string, Entry>();\n  const listeners = new Set<() => void>();\n  let snapshot: Snapshot = { steps: 0, failed: false, current: null };\n  let ready = false;\n\n  const publish = () => {\n    let steps = 0;\n    let failed = false;\n    let current: React.ReactNode = null;\n    for (const entry of entries.values()) {\n      if (entry.status !== \"pending\") steps++;\n      if (entry.status === \"error\") failed = true;\n      if (entry.status === \"active\") current = entry.title;\n    }\n    if (snapshot.steps === steps && snapshot.failed === failed && Object.is(snapshot.current, current)) return;\n    snapshot = { steps, failed, current };\n    listeners.forEach((listener) => listener());\n  };\n\n  return {\n    set(id: string, entry: Entry) {\n      entries.set(id, entry);\n      publish();\n    },\n    remove(id: string) {\n      entries.delete(id);\n      publish();\n    },\n    subscribe(listener: () => void) {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n    snapshot: () => snapshot,\n    failed: () => snapshot.failed,\n    /** False until the trail's first commit: steps that mount after it arrive. */\n    ready: () => ready,\n    markReady() {\n      ready = true;\n    },\n  };\n}\n\ntype Store = ReturnType<typeof createStore>;\n\nconst TrailContext = React.createContext<{ store: Store; reduced: boolean } | null>(null);\n\nfunction useTrail() {\n  const trail = React.useContext(TrailContext);\n  if (!trail) throw new Error(\"<ThoughtTrailStep> must be rendered inside <ThoughtTrail>.\");\n  return trail;\n}\n\nconst subscribe = () => () => {};\n\n/* ── ThoughtTrail ───────────────────────────────────────── */\n\nexport interface ThoughtTrailDoneInfo {\n  /** How long the run took, rounded to whole seconds: `duration`, or the trail's own clock. Null when neither knows. */\n  seconds: number | null;\n  /** Steps that started. */\n  steps: number;\n  /** A step ended with `status=\"error\"`. */\n  failed: boolean;\n}\n\nexport interface ThoughtTrailProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** The agent is working. The header shimmers and counts seconds; when it turns false, the trail finishes. */\n  running?: boolean;\n  /** Header text while running. */\n  label?: React.ReactNode;\n  /** Header text once finished. Defaults to \"Worked for 14s · 5 steps\", or \"Stopped after 9s\" when a step failed. */\n  doneLabel?: (info: ThoughtTrailDoneInfo) => React.ReactNode;\n  /** Seconds the run took, for a trail restored from history. Overrides the trail's own clock. */\n  duration?: number;\n  /** Fold the trail into its header a beat after it finishes. Failed runs stay open. */\n  collapseOnDone?: boolean;\n  /** Open on mount. Defaults to `running || !collapseOnDone`, so a finished trail from history starts folded. */\n  defaultOpen?: boolean;\n  /** Control the fold yourself. The trail then never opens or folds on its own. */\n  open?: boolean;\n  /** Called when the reader clicks the header. */\n  onOpenChange?: (open: boolean) => void;\n  /** Shown before the header text, in the column of the step dots. */\n  icon?: React.ReactNode;\n}\n\n/**\n * An agent's steps as they happen. Each step shows its details while it's\n * active, then shrinks to a one-line summary when it's done.\n */\nexport function ThoughtTrail({\n  running = false,\n  label = \"Working\",\n  doneLabel = defaultDoneLabel,\n  duration,\n  collapseOnDone = true,\n  defaultOpen,\n  open: openProp,\n  onOpenChange,\n  icon,\n  className,\n  children,\n  ...props\n}: ThoughtTrailProps) {\n  const [store] = React.useState(createStore);\n  // False while rendering on the server and hydrating that HTML.\n  const hydrated = React.useSyncExternalStore(subscribe, () => true, () => false);\n  const reduced = (useReducedMotion() ?? false) && hydrated;\n  const failed = React.useSyncExternalStore(store.subscribe, store.failed, store.failed);\n  const bodyId = React.useId();\n\n  React.useEffect(() => store.markReady(), [store]);\n\n  const [openState, setOpenState] = React.useState(defaultOpen ?? (running || !collapseOnDone));\n  const [touched, setTouched] = React.useState(false);\n  const [run, setRun] = React.useState({ running, failed, ended: false });\n  if (run.running !== running || run.failed !== failed) {\n    const started = running && !run.running;\n    setRun({ running, failed, ended: started ? false : run.ended || (!running && run.running) });\n    // A new run opens the trail, and so does a failure.\n    if (started) {\n      setTouched(false);\n      setOpenState(true);\n    } else if (failed && !run.failed) {\n      setOpenState(true);\n    }\n  }\n\n  // A run that finishes folds into its header after a beat, unless the reader took over.\n  const fold = openProp === undefined && collapseOnDone && run.ended && !failed && !touched && openState;\n  React.useEffect(() => {\n    if (!fold) return;\n    const timer = window.setTimeout(() => setOpenState(false), TRAIL.settle * 1000);\n    return () => window.clearTimeout(timer);\n  }, [fold]);\n\n  const open = openProp ?? openState;\n  const toggle = () => {\n    setTouched(true);\n    setOpenState(!open);\n    onOpenChange?.(!open);\n  };\n\n  const trail = React.useMemo(() => ({ store, reduced }), [store, reduced]);\n\n  return (\n    <TrailContext.Provider value={trail}>\n      <div className={cn(\"w-full min-w-0 text-sm\", className)} {...props}>\n        <Header\n          store={store}\n          running={running}\n          open={open}\n          onToggle={toggle}\n          controls={bodyId}\n          label={label}\n          doneLabel={doneLabel}\n          duration={duration}\n          icon={icon}\n          reduced={reduced}\n        />\n        <Fold id={bodyId} open={open} reduced={reduced}>\n          <ol className=\"pt-1\">{children}</ol>\n        </Fold>\n      </div>\n    </TrailContext.Provider>\n  );\n}\n\nfunction defaultDoneLabel({ seconds, steps, failed }: ThoughtTrailDoneInfo) {\n  const time = seconds === null ? null : formatSeconds(seconds);\n  const head = failed ? (time ? `Stopped after ${time}` : \"Stopped\") : time ? `Worked for ${time}` : \"Done\";\n  return steps > 0 ? `${head} · ${steps} ${steps === 1 ? \"step\" : \"steps\"}` : head;\n}\n\nfunction formatSeconds(seconds: number) {\n  if (seconds < 60) return `${seconds}s`;\n  return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, \"0\")}s`;\n}\n\n/** Seconds since `running` last turned true, read every TRAIL.tick ms and held once it turns false. */\nfunction useClock(running: boolean) {\n  const [clock, setClock] = React.useState<{ running: boolean; seconds: number | null }>({\n    running,\n    seconds: null,\n  });\n  // A new run starts from zero, before its first reading.\n  if (clock.running !== running) setClock({ running, seconds: running ? 0 : clock.seconds });\n\n  React.useEffect(() => {\n    if (!running) return;\n    const start = performance.now();\n    const read = () => setClock({ running: true, seconds: (performance.now() - start) / 1000 });\n    const timer = window.setInterval(read, TRAIL.tick);\n    return () => {\n      window.clearInterval(timer);\n      const seconds = (performance.now() - start) / 1000;\n      setClock({ running: false, seconds });\n    };\n  }, [running]);\n\n  return clock.seconds;\n}\n\nfunction Header({\n  store,\n  running,\n  open,\n  onToggle,\n  controls,\n  label,\n  doneLabel,\n  duration,\n  icon,\n  reduced,\n}: {\n  store: Store;\n  running: boolean;\n  open: boolean;\n  onToggle: () => void;\n  controls: string;\n  label: React.ReactNode;\n  doneLabel: (info: ThoughtTrailDoneInfo) => React.ReactNode;\n  duration: number | undefined;\n  icon: React.ReactNode;\n  reduced: boolean;\n}) {\n  const { steps, failed, current } = React.useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);\n  const seconds = useClock(running);\n  const done = doneLabel({\n    seconds: duration ?? (seconds === null ? null : Math.max(1, Math.round(seconds))),\n    steps,\n    failed,\n  });\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        aria-controls={controls}\n        onClick={onToggle}\n        className={cn(\n          \"flex h-7 max-w-full items-center gap-2 text-left transition-colors duration-200\",\n          FOCUS,\n          running\n            ? TONE.active\n            : failed\n              ? TONE.error\n              : \"text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100\",\n        )}\n      >\n        {icon ? (\n          <span aria-hidden className=\"flex size-5 shrink-0 items-center justify-center [&_svg]:size-4\">\n            {icon}\n          </span>\n        ) : null}\n        <Swap id={running ? \"running\" : \"done\"} reduced={reduced} className=\"min-w-0\">\n          {running ? (\n            <span className=\"flex min-w-0 items-baseline gap-1.5\">\n              <Shimmer active reduced={reduced} className=\"shrink-0\">\n                {label}\n              </Shimmer>\n              <span className=\"shrink-0 text-xs text-zinc-400 tabular-nums dark:text-zinc-500\">\n                {formatSeconds(Math.floor(seconds ?? 0))}\n              </span>\n              {/* Folded by hand mid-run: keep the current step in view. */}\n              {!open && current ? (\n                <>\n                  <span aria-hidden className=\"text-zinc-300 dark:text-zinc-600\">\n                    ·\n                  </span>\n                  <Shimmer active reduced={reduced} className=\"min-w-0\">\n                    {current}\n                  </Shimmer>\n                </>\n              ) : null}\n            </span>\n          ) : (\n            <span className=\"block truncate\">{done}</span>\n          )}\n        </Swap>\n        <Chevron open={open} />\n      </button>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {running ? current : done}\n      </span>\n    </>\n  );\n}\n\n/* ── ThoughtTrailStep ───────────────────────────────────── */\n\nexport interface ThoughtTrailStepProps {\n  /** Where the step is. `active` shows its details and shimmers; `done` shrinks it to its summary. */\n  status?: ThoughtTrailStatus;\n  /** What the step is doing, e.g. \"Searching the docs\". Shimmers while active. */\n  title: React.ReactNode;\n  /** What it did, e.g. \"Read 4 pages\". Replaces the title once done (or failed). */\n  summary?: React.ReactNode;\n  /** Replaces the dot and check. While active, the working arc circles it. */\n  icon?: React.ReactNode;\n  /** Small text at the end of the row: a duration, a count. */\n  meta?: React.ReactNode;\n  /** Classes for the details box. Give it `max-h-40 overflow-y-auto` and, while active, it keeps the newest line in view. */\n  contentClassName?: string;\n  className?: string;\n  /** The details: streamed text, logs, sources. Shown while active or failed, and when the reader opens the step. */\n  children?: React.ReactNode;\n}\n\nexport function ThoughtTrailStep({\n  status = \"done\",\n  title,\n  summary,\n  icon,\n  meta,\n  contentClassName,\n  className,\n  children,\n}: ThoughtTrailStepProps) {\n  const { store, reduced } = useTrail();\n  const id = React.useId();\n  const detailsId = `${id}details`;\n  // Steps in the trail's first render are just there; later ones arrive.\n  const [arrives] = React.useState(() => store.ready() && !reduced);\n\n  // Null until the reader clicks; until then the step opens while it works.\n  const [chosen, setChosen] = React.useState<boolean | null>(null);\n  const [seen, setSeen] = React.useState(status);\n  if (seen !== status) {\n    setSeen(status);\n    // A failure shows its details, whatever the reader did before.\n    if (status === \"error\") setChosen(null);\n  }\n\n  const hasDetails = children !== undefined && children !== null && children !== false;\n  const canOpen = hasDetails && status !== \"pending\";\n  const open = canOpen && (chosen ?? (status === \"active\" || status === \"error\"));\n  const showSummary = (status === \"done\" || status === \"error\") && summary != null;\n\n  React.useLayoutEffect(() => store.set(id, { status, title }));\n  React.useLayoutEffect(() => () => store.remove(id), [store, id]);\n\n  const label = (\n    // Keyed on the status, so every change of state rolls the line.\n    <Swap id={showSummary ? \"summary\" : status} reduced={reduced} className=\"flex-1\">\n      {showSummary ? (\n        <span className=\"block truncate\">{summary}</span>\n      ) : (\n        <Shimmer active={status === \"active\"} reduced={reduced}>\n          {title}\n        </Shimmer>\n      )}\n    </Swap>\n  );\n  const row = cn(\n    \"col-start-2 row-start-1 flex h-7 min-w-0 items-center gap-2 text-left transition-colors duration-200\",\n    TONE[status],\n  );\n  const extra = meta ? (\n    <span className=\"shrink-0 text-xs text-zinc-400 tabular-nums dark:text-zinc-500\">{meta}</span>\n  ) : null;\n\n  return (\n    <Fold as=\"li\" open appear={arrives} reduced={reduced} className={cn(\"group/step relative\", className)}>\n      <div className=\"grid grid-cols-[1.25rem_minmax(0,1fr)] gap-x-2\">\n        <Rail status={status} reduced={reduced} />\n        <Dot status={status} icon={icon} reduced={reduced} />\n        {canOpen ? (\n          <button\n            type=\"button\"\n            aria-expanded={open}\n            aria-controls={detailsId}\n            onClick={() => setChosen(!open)}\n            className={cn(\n              row,\n              FOCUS,\n              \"group/row\",\n              status === \"done\" && \"hover:text-zinc-900 dark:hover:text-zinc-100\",\n            )}\n          >\n            {label}\n            {extra}\n            <Chevron\n              open={open}\n              className={cn(\n                !open &&\n                  \"opacity-0 group-hover/row:opacity-100 group-focus-visible/row:opacity-100 [@media(hover:none)]:opacity-60\",\n              )}\n            />\n          </button>\n        ) : (\n          <div className={row}>\n            {label}\n            {extra}\n          </div>\n        )}\n        {hasDetails ? (\n          <Fold id={detailsId} open={open} smooth reduced={reduced} className=\"col-start-2\">\n            <Details pinned={status === \"active\"} className={contentClassName}>\n              {children}\n            </Details>\n          </Fold>\n        ) : null}\n      </div>\n    </Fold>\n  );\n}\n\n/** The line down to the next step. It fills in once this step is done. */\nfunction Rail({ status, reduced }: { status: ThoughtTrailStatus; reduced: boolean }) {\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        // From just under this dot to just above the next, which sits 7px into its row.\n        \"pointer-events-none absolute top-6 -bottom-1 left-[calc(0.625rem-0.5px)] w-px overflow-hidden rounded-full transition-[opacity,background-color] duration-300 group-last/step:opacity-0\",\n        status === \"error\" ? \"bg-rose-200 dark:bg-rose-500/30\" : \"bg-zinc-200 dark:bg-zinc-800\",\n      )}\n    >\n      <motion.span\n        className=\"absolute inset-0 origin-top bg-zinc-400 dark:bg-zinc-500\"\n        initial={false}\n        animate={{ scaleY: status === \"done\" ? 1 : 0 }}\n        transition={reduced ? { duration: 0 } : TRAIL.rail}\n      />\n    </span>\n  );\n}\n\nfunction Dot({\n  status,\n  icon,\n  reduced,\n}: {\n  status: ThoughtTrailStatus;\n  icon: React.ReactNode;\n  reduced: boolean;\n}) {\n  const key = status === \"error\" ? \"error\" : icon ? (status === \"active\" ? \"icon-active\" : \"icon\") : status;\n  let glyph: React.ReactNode;\n  if (status === \"error\") glyph = <Cross reduced={reduced} />;\n  else if (icon)\n    glyph = (\n      <span className=\"relative flex size-5 items-center justify-center\">\n        <span className=\"flex [&_svg]:size-3.5\">{icon}</span>\n        {status === \"active\" ? <Spinner reduced={reduced} className=\"absolute inset-0 size-5\" ring /> : null}\n      </span>\n    );\n  else if (status === \"active\") glyph = <Spinner reduced={reduced} className=\"size-3.5\" />;\n  else if (status === \"done\") glyph = <Check reduced={reduced} />;\n  else glyph = <span className=\"size-2 rounded-full border border-current\" />;\n\n  return (\n    <span aria-hidden className=\"relative col-start-1 row-start-1 flex h-7 items-center justify-center\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          key={key}\n          className={cn(\"flex size-5 items-center justify-center transition-colors duration-200\", DOT_TONE[status])}\n          initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n          transition={reduced ? { duration: 0.15 } : TRAIL.pop}\n        >\n          {glyph}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction Spinner({ reduced, ring = false, className }: { reduced: boolean; ring?: boolean; className?: string }) {\n  return (\n    <motion.svg\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      className={className}\n      // Reduced motion: a slow breath instead of a turn.\n      animate={reduced ? { opacity: [1, 0.35, 1] } : { rotate: 360 }}\n      transition={\n        reduced\n          ? { duration: 1.6, ease: \"easeInOut\", repeat: Infinity }\n          : { duration: TRAIL.spin, ease: \"linear\", repeat: Infinity }\n      }\n    >\n      <circle cx=\"10\" cy=\"10\" r={ring ? 8.5 : 7} stroke=\"currentColor\" strokeOpacity=\"0.18\" strokeWidth={ring ? 1.25 : 2} />\n      <path\n        d={ring ? \"M10 1.5a8.5 8.5 0 0 1 8.5 8.5\" : \"M10 3a7 7 0 0 1 7 7\"}\n        stroke=\"currentColor\"\n        strokeWidth={ring ? 1.25 : 2}\n        strokeLinecap=\"round\"\n      />\n    </motion.svg>\n  );\n}\n\nfunction Check({ reduced }: { reduced: boolean }) {\n  return (\n    <svg viewBox=\"0 0 20 20\" fill=\"none\" className=\"size-3.5\">\n      <motion.path\n        d=\"M4.5 10.5l3.5 3.5 7.5-8\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        initial={{ pathLength: reduced ? 1 : 0 }}\n        animate={{ pathLength: 1 }}\n        transition={TRAIL.draw}\n      />\n    </svg>\n  );\n}\n\nfunction Cross({ reduced }: { reduced: boolean }) {\n  return (\n    <motion.svg\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      className=\"size-3.5\"\n      animate={reduced ? undefined : { x: [0, -2.5, 2.5, -1.5, 0] }}\n      transition={{ duration: 0.4, delay: 0.15 }}\n    >\n      {[\"M5.5 5.5l9 9\", \"M14.5 5.5l-9 9\"].map((d, i) => (\n        <motion.path\n          key={d}\n          d={d}\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          initial={{ pathLength: reduced ? 1 : 0 }}\n          animate={{ pathLength: 1 }}\n          transition={{ ...TRAIL.draw, delay: i * 0.08 }}\n        />\n      ))}\n    </motion.svg>\n  );\n}\n\nfunction Chevron({ open, className }: { open: boolean; className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      aria-hidden\n      className={cn(\n        \"size-3.5 shrink-0 text-zinc-400 transition-[rotate,opacity] duration-200 ease-out dark:text-zinc-500\",\n        open ? \"rotate-0\" : \"-rotate-90\",\n        className,\n      )}\n    >\n      <path d=\"M4 6l4 4 4-4\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\n/* ── Building blocks ────────────────────────────────────── */\n\n/** Text that a band of light sweeps across while `active`. Inherits its color. */\nfunction Shimmer({\n  active,\n  reduced,\n  className,\n  children,\n}: {\n  active: boolean;\n  reduced: boolean;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  // Inline-block, so the band crosses the words rather than the whole row.\n  const base = cn(\"inline-block max-w-full truncate align-top\", className);\n  if (!active || reduced) return <span className={base}>{children}</span>;\n  return (\n    <motion.span\n      className={base}\n      style={{\n        backgroundImage: SHEEN,\n        backgroundSize: \"250% 100%\",\n        backgroundClip: \"text\",\n        WebkitBackgroundClip: \"text\",\n        // Not `color`: the gradient is drawn in currentColor.\n        WebkitTextFillColor: \"transparent\",\n      }}\n      initial={{ backgroundPosition: \"100% 0%\" }}\n      animate={{ backgroundPosition: \"0% 0%\" }}\n      transition={{ duration: TRAIL.sheen, ease: \"linear\", repeat: Infinity, repeatDelay: 0.2 }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n\n/** One line of text that rolls up into the next when `id` changes. */\nfunction Swap({\n  id,\n  reduced,\n  className,\n  children,\n}: {\n  id: string;\n  reduced: boolean;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <span className={cn(\"relative block min-w-0\", className)}>\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          key={id}\n          className=\"block min-w-0\"\n          initial={reduced ? { opacity: 0 } : { opacity: 0, y: \"0.75em\", filter: \"blur(2px)\" }}\n          animate={{ opacity: 1, y: \"0em\", filter: \"blur(0px)\" }}\n          exit={reduced ? { opacity: 0 } : { opacity: 0, y: \"-0.75em\", filter: \"blur(2px)\" }}\n          transition={reduced ? { duration: 0.15 } : TRAIL.roll}\n        >\n          {children}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/** A step's details. While pinned, a scrolling box keeps its newest line in view, until the reader scrolls up. */\nfunction Details({\n  pinned,\n  className,\n  children,\n}: {\n  pinned: boolean;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  React.useEffect(() => {\n    const box = ref.current;\n    if (!box || !pinned) return;\n    let stuck = true;\n    const follow = () => {\n      if (stuck) box.scrollTop = box.scrollHeight;\n    };\n    const onScroll = () => {\n      stuck = box.scrollHeight - box.scrollTop - box.clientHeight < 16;\n    };\n    const observer = new MutationObserver(follow);\n    observer.observe(box, { childList: true, subtree: true, characterData: true });\n    box.addEventListener(\"scroll\", onScroll, { passive: true });\n    follow();\n    return () => {\n      observer.disconnect();\n      box.removeEventListener(\"scroll\", onScroll);\n    };\n  }, [pinned]);\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\"pt-0.5 pb-3 text-[13px] leading-relaxed text-zinc-500 dark:text-zinc-400\", className)}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * Opens and closes its content by animating height. Clipped only while it\n * moves, so focus rings and the rail can spill out at rest. At rest an open\n * fold is `auto` and follows its content at once; a `smooth` one stays on a\n * number and springs after it instead, for content that streams in.\n */\nfunction Fold({\n  open,\n  appear = false,\n  smooth = false,\n  reduced,\n  as = \"div\",\n  id,\n  className,\n  children,\n}: {\n  open: boolean;\n  /** Grow in from nothing on mount. */\n  appear?: boolean;\n  smooth?: boolean;\n  reduced: boolean;\n  as?: \"div\" | \"li\";\n  id?: string;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const innerRef = React.useRef<HTMLDivElement>(null);\n  const shown = open && !appear;\n  const height = useMotionValue<number | \"auto\">(shown ? \"auto\" : 0);\n  const overflow = useMotionValue(shown ? \"visible\" : \"hidden\");\n  const opacity = useMotionValue(shown ? 1 : 0);\n  const y = useTransform(opacity, [0, 1], [-TRAIL.drift, 0]);\n  const state = React.useRef({ open: false, mounted: false, moving: false, target: -1, reduced, smooth });\n\n  React.useLayoutEffect(() => {\n    state.current.reduced = reduced;\n    state.current.smooth = smooth;\n  });\n\n  // Animate to `to`, clipped on the way.\n  const glide = React.useCallback(\n    (to: number, transition: Transition) => {\n      const s = state.current;\n      const inner = innerRef.current;\n      if (!inner || (s.moving && s.target === to)) return;\n      s.target = to;\n      if (height.get() === \"auto\") height.jump(inner.offsetHeight);\n      // Nothing to animate: settle where it is.\n      if (s.reduced || (!s.moving && height.get() === to)) {\n        s.moving = false;\n        height.jump(to === 0 || s.smooth ? to : \"auto\");\n        overflow.set(to === 0 ? \"hidden\" : \"visible\");\n        return;\n      }\n      s.moving = true;\n      overflow.set(\"hidden\");\n      animate(height, to, {\n        ...transition,\n        onComplete: () => {\n          if (s.target !== to) return;\n          s.moving = false;\n          if (to === 0) return;\n          if (!s.smooth) height.jump(\"auto\");\n          overflow.set(\"visible\");\n        },\n      });\n    },\n    [height, overflow],\n  );\n\n  React.useLayoutEffect(() => {\n    const s = state.current;\n    const inner = innerRef.current;\n    if (!inner) return;\n    const first = !s.mounted;\n    s.mounted = true;\n    s.open = open;\n    if (first && !(open && appear)) {\n      // Already at rest; a smooth fold swaps `auto` for its number.\n      if (open && smooth) {\n        s.target = inner.offsetHeight;\n        height.jump(s.target);\n      }\n      return;\n    }\n    glide(open ? inner.offsetHeight : 0, TRAIL.fold);\n    if (s.reduced) opacity.jump(open ? 1 : 0);\n    else animate(opacity, open ? 1 : 0, open ? TRAIL.show : TRAIL.hide);\n    // Mount-time values only: `appear` and `smooth` are fixed per fold.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open, glide, height, opacity]);\n\n  // Content that changes size: a fold on the move retargets, a smooth one follows.\n  React.useLayoutEffect(() => {\n    const inner = innerRef.current;\n    if (!inner) return;\n    const observer = new ResizeObserver(() => {\n      const s = state.current;\n      if (!s.open || !(s.moving || s.smooth)) return;\n      glide(inner.offsetHeight, s.moving && !s.smooth ? TRAIL.fold : TRAIL.grow);\n    });\n    observer.observe(inner);\n    return () => observer.disconnect();\n  }, [glide]);\n\n  const content = (\n    <motion.div ref={innerRef} className=\"flow-root\" style={{ opacity, y: reduced ? 0 : y }}>\n      {children}\n    </motion.div>\n  );\n  const style = { height, overflow } as { height: MotionValue<number | \"auto\">; overflow: MotionValue<string> };\n  return as === \"li\" ? (\n    <motion.li id={id} inert={!open} className={className} style={style}>\n      {content}\n    </motion.li>\n  ) : (\n    <motion.div id={id} inert={!open} className={className} style={style}>\n      {content}\n    </motion.div>\n  );\n}\n"
    }
  ]
}