{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "autopilot",
  "type": "registry:ui",
  "title": "Autopilot",
  "description": "A cursor that uses your product on its own. It glides to buttons and presses them, types and drags across real markup on a script you write, and steps aside the moment a visitor wants to try it.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/autopilot.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  motionValue,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * AUTOPILOT STORYBOARD\n *\n *  rest     your UI, untouched, with a cursor (or a few) at home.\n *           Each trails a pill in its own color: a name, or what\n *           it's doing right now\n *  move     it glides to the next target on a gentle arc, easing\n *           in and out: about 350ms for a hop, 900ms across the\n *           stage. Long trips overshoot a touch and correct.\n *           Targets it passes over get data-autopilot-hover\n *  click    a beat over the target, then the pointer squashes, a\n *           ring spreads from its tip and the target gets\n *           data-autopilot-press. Pointer, mouse and click events\n *           fire on the element, so the UI's own handlers run\n *  type     it clicks into the field, which shows data-autopilot-\n *           focus instead of taking real focus, eases the pointer\n *           off the line, and types at a person's uneven pace: a\n *           pause after spaces and punctuation, the odd burst\n *  drag     the item lifts with a little scale and shadow, rides\n *           the path, and settles into wherever the drop puts it.\n *           The target shows data-autopilot-drop while it's over\n *  say      the old label slides up and out as the new one comes\n *           in, and the pill's width follows\n *  yield    the visitor's pointer or focus comes in: each cursor\n *           finishes the key or click it's on, glides to the\n *           corner and fades, reading \"Your turn\". Two seconds\n *           after they leave it glides back and replays the\n *           chapter it was in, from its setup. On touch screens a\n *           tap takes over and Resume hands it back\n *  loop     after the last chapter the finished state holds for\n *           2s, the stage crossfades back to the start and the\n *           cursors glide home\n *  pause    off screen, in a background tab, or with the pause\n *           button. The chapter strip fills like a story timer;\n *           click a chapter to jump to it\n *  reduced  no autoplay: the first state with a Play button. When\n *           played, cursors jump between targets, with no ripple,\n *           overshoot or settle\n *\n *  Cursors move on motion values, outside React. The script is\n *  one async run that a takeover, a pause or a jump aborts; the\n *  next run waits for it to finish the action it's in.\n * ───────────────────────────────────────────────────────── */\n\nconst AUTOPILOT = {\n  hop: 350, // ms to move 40px\n  trip: 900, // ms to move 800px. Moves in between follow Fitts's law\n  quickest: 200, // ms, the shortest move\n  bow: 0.12, // how far a path bows off the straight line, as a share of its length\n  overshootFrom: 240, // px; longer moves overshoot and correct\n  overshoot: 0.035, // share of the distance a long move overshoots by\n  overshootMax: 14, // px\n  correct: 150, // ms the correction takes\n  beat: 140, // ms over a target before it presses\n  press: 90, // ms a button is held down\n  squash: 0.84, // pointer scale while pressed\n  ripple: 0.45, // s the click ring takes to spread and fade\n  key: 70, // ms between keys, on average\n  space: 60, // ms more after a space\n  stop: 200, // ms more after punctuation\n  burst: 0.15, // chance the next word comes out in a quick burst\n  lift: 1.04, // scale of a dragged item\n  carry: 1.2, // a drag takes this much longer than a bare move\n  settle: { type: \"spring\", stiffness: 520, damping: 30 },\n  yield: 320, // ms to glide aside when the visitor takes over\n  resume: 2000, // ms after the visitor leaves before it carries on\n  hold: 2000, // ms the finished state holds before the loop starts over\n  fade: 0.3, // s the reset crossfade takes\n  parked: 0.45, // the pointer's opacity while parked; its label stays readable\n  find: 2000, // ms to wait for a target that isn't on screen yet\n  label: { type: \"spring\", visualDuration: 0.3, bounce: 0 },\n} as const;\n\n// The pill sits under the arrow's tail, kept inside the stage.\nconst PILL = { x: 12, y: 20, height: 24, inset: 4 } as const;\n\n/* ── Script ─────────────────────────────────────────────── */\n\nexport type AutopilotCursor = {\n  /** Steps name their cursor with this. */\n  id: string;\n  /** The pill's text when the cursor isn't saying anything: a name, or \"Agent\". */\n  label: string;\n  /** The pointer's and pill's color. The pill's text is white. */\n  color: string;\n  /** Where it waits at the start and between loops, as fractions of the stage. Defaults to the corner it parks in. */\n  home?: [number, number];\n};\n\ntype StepBase = {\n  /** The cursor that takes this step. Defaults to the first one. */\n  cursor?: string;\n  /** Changes the cursor's label as the step starts, and keeps it. `null` goes back to its name. */\n  say?: string | null;\n};\n\n/** Where on the target to point, as fractions of its width and height. Defaults to near the middle. */\ntype Aim = { at?: [number, number] };\n\nexport type AutopilotStep = StepBase &\n  (\n    | ({ move: string } & Aim)\n    | ({ click: string } & Aim)\n    | ({ type: string; into: string } & Aim)\n    | ({ drag: string; to: string; drop?: () => void } & Aim)\n    | { wait: number }\n    | { do: () => void | Promise<void> }\n  );\n\nexport type AutopilotChapter = {\n  title: string;\n  /** Puts the UI in the state this chapter starts from. Runs every time the chapter starts. */\n  setup?: () => void;\n  steps: AutopilotStep[];\n};\n\n/* ── Motion helpers ─────────────────────────────────────── */\n\ntype Point = { x: number; y: number };\ntype Box = { left: number; top: number; right: number; bottom: number; width: number; height: number };\ntype Flag = \"hover\" | \"press\" | \"focus\" | \"drag\" | \"drop\";\ntype Field = HTMLInputElement | HTMLTextAreaElement;\n\ntype Rig = {\n  x: MotionValue<number>;\n  y: MotionValue<number>;\n  opacity: MotionValue<number>;\n  rest: MotionValue<number>; // 0 → 1 as it parks\n  squash: MotionValue<number>;\n  ring: MotionValue<number>; // 0 → 1 as a click ring spreads\n  /** Ends the glide in progress where it is. */\n  stop: () => void;\n  hover: Element | null;\n  focus: Element | null;\n  /** Where it rests, so it can follow the stage when it resizes. */\n  placed: \"home\" | \"park\" | null;\n};\n\nconst noop = () => {};\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\nconst vary = (value: number, spread: number) => value * (1 - spread + Math.random() * spread * 2);\n\n// Minimum jerk: the bell-shaped speed of a hand reaching for something.\nconst reach = (t: number) => t * t * t * (10 - 15 * t + 6 * t * t);\nconst EASE_OUT = [0.22, 1, 0.36, 1] as const;\n\n// Move time grows with the log of the distance, through `hop` at 40px and `trip` at 800px.\nconst FITTS = (AUTOPILOT.trip - AUTOPILOT.hop) / Math.log2(41 / 3);\nconst travel = (distance: number) =>\n  Math.max(AUTOPILOT.quickest, AUTOPILOT.hop + FITTS * Math.log2((1 + distance / 20) / 3));\n\nconst tip = (rig: Rig): Point => ({ x: rig.x.get(), y: rig.y.get() });\nconst inside = (box: Box, p: Point, inset = 2) =>\n  p.x >= box.left + inset && p.x <= box.right - inset && p.y >= box.top + inset && p.y <= box.bottom - inset;\n\n/** The control point of a path that bows toward the top left, the way a wrist swings, now and then the other way. */\nfunction bend(from: Point, to: Point, bow: number): Point {\n  const dx = to.x - from.x;\n  const dy = to.y - from.y;\n  const length = Math.hypot(dx, dy);\n  const mid = { x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 };\n  if (!length || !bow) return mid;\n  let nx = -dy / length;\n  let ny = dx / length;\n  if (nx + ny > 0) {\n    nx = -nx;\n    ny = -ny;\n  }\n  const side = Math.random() < 0.2 ? -1 : 1;\n  const amount = length * vary(bow, 0.35) * side * (length < 80 ? 0.5 : 1);\n  return { x: mid.x + nx * amount, y: mid.y + ny * amount };\n}\n\nfunction sleep(ms: number, signal?: AbortSignal) {\n  return new Promise<void>((resolve, reject) => {\n    if (signal?.aborted) return reject(signal.reason);\n    const cancel = () => {\n      window.clearTimeout(timer);\n      reject(signal?.reason);\n    };\n    const timer = window.setTimeout(() => {\n      signal?.removeEventListener(\"abort\", cancel);\n      resolve();\n    }, ms);\n    signal?.addEventListener(\"abort\", cancel, { once: true });\n  });\n}\n\nfunction nextFrame(signal?: AbortSignal) {\n  return new Promise<void>((resolve, reject) => {\n    if (signal?.aborted) return reject(signal.reason);\n    const cancel = () => {\n      cancelAnimationFrame(id);\n      reject(signal?.reason);\n    };\n    const id = requestAnimationFrame(() => {\n      signal?.removeEventListener(\"abort\", cancel);\n      resolve();\n    });\n    signal?.addEventListener(\"abort\", cancel, { once: true });\n  });\n}\n\n/**\n * Moves a cursor along a quadratic curve. Resolves when it arrives, or when\n * another glide takes over; rejects if the signal aborts first.\n */\nfunction glide(\n  rig: Rig,\n  to: Point,\n  ms: number,\n  {\n    signal,\n    bow = 0,\n    ease = reach,\n    onFrame,\n  }: {\n    signal?: AbortSignal;\n    bow?: number;\n    ease?: ((t: number) => number) | typeof EASE_OUT;\n    onFrame?: (p: Point) => void;\n  } = {},\n) {\n  rig.stop();\n  const from = tip(rig);\n  const via = bend(from, to, bow);\n  return new Promise<void>((resolve, reject) => {\n    if (signal?.aborted) return reject(signal.reason);\n    const settle = () => {\n      signal?.removeEventListener(\"abort\", cancel);\n      rig.stop = noop;\n    };\n    const cancel = () => {\n      controls.stop();\n      settle();\n      reject(signal?.reason);\n    };\n    const done = () => {\n      settle();\n      resolve();\n    };\n    const controls = animate(0, 1, {\n      duration: Math.max(ms, 1) / 1000,\n      ease,\n      onUpdate: (t) => {\n        const u = 1 - t;\n        const p = {\n          x: u * u * from.x + 2 * u * t * via.x + t * t * to.x,\n          y: u * u * from.y + 2 * u * t * via.y + t * t * to.y,\n        };\n        rig.x.set(p.x);\n        rig.y.set(p.y);\n        onFrame?.(p);\n      },\n      onComplete: done,\n    });\n    rig.stop = () => {\n      controls.stop();\n      done();\n    };\n    signal?.addEventListener(\"abort\", cancel, { once: true });\n  });\n}\n\n/** Mouse and pointer events with real coordinates, so handlers that read them work. */\nfunction fire(target: Element, type: string, at: Point) {\n  const down = type.endsWith(\"down\");\n  const init = {\n    bubbles: true,\n    cancelable: true,\n    composed: true,\n    view: window,\n    clientX: at.x,\n    clientY: at.y,\n    screenX: at.x + window.screenX,\n    screenY: at.y + window.screenY,\n    button: 0,\n    buttons: down ? 1 : 0,\n    detail: 1,\n  };\n  target.dispatchEvent(\n    type.startsWith(\"pointer\")\n      ? new PointerEvent(type, {\n          ...init,\n          pointerId: 41, // not a real pointer, so it never collides with the visitor's\n          pointerType: \"mouse\",\n          isPrimary: true,\n          width: 1,\n          height: 1,\n          pressure: down ? 0.5 : 0,\n        })\n      : new MouseEvent(type, init),\n  );\n}\n\nconst fieldIn = (element: Element): Field | null =>\n  element.matches(\"input, textarea\")\n    ? (element as Field)\n    : element.querySelector<Field>(\"input, textarea\");\n\n// React tracks an input's last value; setting it through the prototype lets onChange see the change.\nfunction setValue(field: Field, value: string, inputType: string, data: string | null) {\n  const proto = field instanceof HTMLTextAreaElement ? HTMLTextAreaElement : HTMLInputElement;\n  Object.getOwnPropertyDescriptor(proto.prototype, \"value\")?.set?.call(field, value);\n  field.dispatchEvent(new InputEvent(\"input\", { bubbles: true, inputType, data }));\n}\n\nfunction press(field: Field, key: string) {\n  const init = { key, bubbles: true, cancelable: true, composed: true };\n  if (field.dispatchEvent(new KeyboardEvent(\"keydown\", init))) {\n    if (key === \"Backspace\") setValue(field, field.value.slice(0, -1), \"deleteContentBackward\", null);\n    else if (key !== \"Enter\") setValue(field, field.value + key, \"insertText\", key);\n  }\n  field.dispatchEvent(new KeyboardEvent(\"keyup\", init));\n}\n\nconst KEYS = /\\{(?:Enter|Backspace)\\}|[\\s\\S]/gu;\nconst keysOf = (text: string) => (text.match(KEYS) ?? []).map((key) => key.replace(/^\\{(.+)\\}$/, \"$1\"));\n\n/** Holds a dragged item up: lifted on its own layer, out of the way of hit tests. */\nfunction lift(element: HTMLElement) {\n  const props = [\"translate\", \"scale\", \"box-shadow\", \"z-index\", \"position\", \"pointer-events\", \"transition\"];\n  const saved = props.map((prop) => [prop, element.style.getPropertyValue(prop)] as const);\n  const computed = getComputedStyle(element);\n  const own = computed.boxShadow === \"none\" ? \"\" : `, ${computed.boxShadow}`;\n  element.style.transition = \"none\";\n  element.style.pointerEvents = \"none\";\n  element.style.zIndex = \"50\";\n  if (computed.position === \"static\") element.style.position = \"relative\";\n  let offset: Point = { x: 0, y: 0 };\n  let raised = 0;\n  const paint = () => {\n    const p = Math.max(raised, 0);\n    element.style.translate = `${offset.x}px ${offset.y}px`;\n    element.style.scale = `${1 + (AUTOPILOT.lift - 1) * raised}`;\n    element.style.boxShadow = `0 ${16 * p}px ${32 * p}px -12px rgb(0 0 0 / ${0.3 * p}), 0 ${5 * p}px ${12 * p}px -6px rgb(0 0 0 / ${0.16 * p})${own}`;\n  };\n  return {\n    element,\n    move(x: number, y: number) {\n      offset = { x, y };\n      paint();\n    },\n    raise(p: number) {\n      raised = p;\n      paint();\n    },\n    restore() {\n      for (const [prop, value] of saved) {\n        if (value) element.style.setProperty(prop, value);\n        else element.style.removeProperty(prop);\n      }\n    },\n  };\n}\n\n/** A still copy of the stage laid over it, to crossfade from while the live UI resets underneath. */\nfunction snapshot(content: HTMLElement, host: HTMLElement) {\n  const copy = content.cloneNode(true) as HTMLElement; // inputs keep their values\n  for (const element of [copy, ...copy.querySelectorAll(\"[id]\")]) element.removeAttribute(\"id\");\n  copy.inert = true;\n  Object.assign(copy.style, {\n    position: \"absolute\",\n    left: `${content.offsetLeft}px`,\n    top: `${content.offsetTop}px`,\n    width: `${content.offsetWidth}px`,\n    height: `${content.offsetHeight}px`,\n    margin: \"0\",\n  });\n  host.append(copy);\n  return copy;\n}\n\nconst warn = (message: string) => console.warn(`Autopilot: ${message}`);\n\n/* ── The pilot ──────────────────────────────────────────── */\n\nexport type AutopilotCorner = \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\";\n\ntype PilotConfig = {\n  cursors: AutopilotCursor[];\n  chapters: AutopilotChapter[];\n  reduced: boolean;\n  loop: boolean;\n  park: AutopilotCorner;\n  /** Whether stopping now means the visitor took over, so the cursors step aside. */\n  yielding: boolean;\n  onChapterChange?: (index: number) => void;\n};\n\n/** How the pilot tells React what to show. */\ntype PilotUI = {\n  onChapter: (index: number) => void;\n  onSay: (id: string, text: string | null) => void;\n  onQuiet: () => void;\n  onParked: (parked: boolean) => void;\n  onFinish: () => void;\n};\n\n/**\n * Everything that moves lives here, outside React: the cursors' motion\n * values, the attributes on the targets, and the run through the script.\n */\nfunction createPilot() {\n  const rigs = new Map<string, Rig>();\n  const flags = new Map<Element, Map<Flag, number>>();\n  const progress = motionValue(0);\n  const width = motionValue(0);\n  const height = motionValue(0);\n  const measured: number[] = []; // how long each chapter took last time, for the strip's timer\n\n  let config: PilotConfig = {\n    cursors: [],\n    chapters: [],\n    reduced: false,\n    loop: true,\n    park: \"bottom-right\",\n    yielding: false,\n  };\n  let ui: PilotUI = { onChapter: noop, onSay: noop, onQuiet: noop, onParked: noop, onFinish: noop };\n  let stage: HTMLElement | null = null;\n  let content: HTMLElement | null = null;\n  let host: HTMLElement | null = null;\n  let chapter = 0; // where the next run starts\n  let pending: number | null = null; // a chapter picked from the strip\n  let ended = false; // the last chapter finished, so the next run starts the loop over\n  let dirty = false; // the UI may have moved on from the chapter's setup\n  let parked = false;\n  let actingUntil = 0;\n  let fill: { stop: () => void } | null = null;\n  let queue = Promise.resolve();\n\n  const rig = (id: string) => {\n    let found = rigs.get(id);\n    if (!found) {\n      found = {\n        x: motionValue(0),\n        y: motionValue(0),\n        opacity: motionValue(0),\n        rest: motionValue(0),\n        squash: motionValue(1),\n        ring: motionValue(1),\n        stop: noop,\n        hover: null,\n        focus: null,\n        placed: null,\n      };\n      rigs.set(id, found);\n    }\n    return found;\n  };\n  const crew = () => config.cursors.map((cursor) => rig(cursor.id));\n\n  /* Geometry, in the stage's own px. */\n  const frame = () => {\n    if (!stage) return { left: 0, top: 0, k: 1 };\n    const r = stage.getBoundingClientRect();\n    const k = stage.offsetWidth ? r.width / stage.offsetWidth : 1;\n    return { left: r.left + stage.clientLeft * k, top: r.top + stage.clientTop * k, k };\n  };\n  const toClient = (p: Point) => {\n    const f = frame();\n    return { x: f.left + p.x * f.k, y: f.top + p.y * f.k };\n  };\n  const boxOf = (element: Element): Box => {\n    const f = frame();\n    const r = element.getBoundingClientRect();\n    const left = (r.left - f.left) / f.k;\n    const top = (r.top - f.top) / f.k;\n    const w = r.width / f.k;\n    const h = r.height / f.k;\n    return { left, top, right: left + w, bottom: top + h, width: w, height: h };\n  };\n  // Parked cursors line up from the corner inward; the first one, with the label, sits innermost on the right\n  // and outermost on the left, clear of the pause button.\n  const spot = (index: number): Point => {\n    const [edge, side] = config.park.split(\"-\");\n    const x = side === \"right\" ? width.get() - 112 - index * 22 : 64 - index * 20;\n    const y = edge === \"top\" ? 16 : height.get() - (side === \"left\" ? 64 : 52);\n    return { x: clamp(x, 8, Math.max(8, width.get() - 8)), y: clamp(y, 8, Math.max(8, height.get() - 8)) };\n  };\n  const home = (index: number): Point => {\n    const at = config.cursors[index]?.home;\n    return at ? { x: width.get() * at[0], y: height.get() * at[1] } : spot(index);\n  };\n\n  /** Points at a target the way a hand would: near the middle, never dead center. Fields get clicked toward their start. */\n  const aim = (element: Element, at?: [number, number]): Point => {\n    const box = boxOf(element);\n    const [fx, fy] = at ?? (element.matches(\"input, textarea\") ? [0.22, 0.5] : [0.5, 0.52]);\n    const jitter = at ? 0 : 1;\n    const x = box.left + box.width * fx + jitter * (Math.random() - 0.5) * Math.min(box.width * 0.3, 12);\n    const y = box.top + box.height * fy + jitter * (Math.random() - 0.5) * Math.min(box.height * 0.3, 6);\n    return { x: clamp(x, box.left + 2, box.right - 2), y: clamp(y, box.top + 2, box.bottom - 2) };\n  };\n\n  /* Attributes on targets, counted so cursors sharing a target don't clear each other. */\n  const flag = (element: Element | null, name: Flag, on: boolean) => {\n    if (!element) return;\n    const counts = flags.get(element) ?? new Map<Flag, number>();\n    const n = (counts.get(name) ?? 0) + (on ? 1 : -1);\n    if (n > 0) counts.set(name, n);\n    else counts.delete(name);\n    if (counts.size) flags.set(element, counts);\n    else flags.delete(element);\n    element.toggleAttribute(`data-autopilot-${name}`, n > 0);\n  };\n  const unflagAll = () => {\n    for (const [element, counts] of flags) {\n      for (const name of counts.keys()) element.removeAttribute(`data-autopilot-${name}`);\n    }\n    flags.clear();\n    for (const r of rigs.values()) {\n      r.hover = null;\n      r.focus = null;\n    }\n  };\n  const hoverOn = (r: Rig, element: Element | null) => {\n    if (r.hover === element) return;\n    flag(r.hover, \"hover\", false);\n    r.hover = element;\n    flag(element, \"hover\", true);\n  };\n  const focusOn = (r: Rig, element: Element | null) => {\n    if (r.focus === element) return;\n    flag(r.focus, \"focus\", false);\n    r.focus = element;\n    flag(element, \"focus\", true);\n  };\n\n  /** The UI under a cursor's tip. The cursors and anything dragged don't take hits. */\n  const hit = (p: Point) => {\n    if (!content) return null;\n    const c = toClient(p);\n    const element = document.elementFromPoint(c.x, c.y);\n    return element && content.contains(element) ? element : null;\n  };\n  const hoverAt = (r: Rig, p: Point) => {\n    const target = hit(p)?.closest(\"[data-autopilot]\") ?? null;\n    hoverOn(r, target && content?.contains(target) ? target : null);\n  };\n\n  // A click can make the UI move focus itself, like a menu focusing its first item. That isn't the visitor.\n  const acted = () => {\n    actingUntil = performance.now() + 120;\n  };\n\n  const find = async (name: string, signal: AbortSignal) => {\n    const until = performance.now() + AUTOPILOT.find;\n    for (;;) {\n      const element = content?.querySelector(`[data-autopilot=\"${CSS.escape(name)}\"]`);\n      if (element && element.getClientRects().length) return element;\n      if (performance.now() > until) {\n        warn(`nothing marked data-autopilot=\"${name}\" showed up.`);\n        return null;\n      }\n      await nextFrame(signal);\n    }\n  };\n\n  /* Moves. */\n  const moveTo = async (\n    r: Rig,\n    to: Point,\n    signal: AbortSignal,\n    { carry = false, onFrame }: { carry?: boolean; onFrame?: (p: Point) => void } = {},\n  ) => {\n    r.placed = null;\n    const step = (p: Point) => {\n      hoverAt(r, p);\n      onFrame?.(p);\n    };\n    if (config.reduced) {\n      r.stop();\n      r.x.jump(to.x);\n      r.y.jump(to.y);\n      step(to);\n      await sleep(AUTOPILOT.beat * 2, signal);\n      return;\n    }\n    const from = tip(r);\n    const distance = Math.hypot(to.x - from.x, to.y - from.y);\n    if (distance < 1) return;\n    const ms = vary(travel(distance), 0.08) * (carry ? AUTOPILOT.carry : 1);\n    if (carry || distance < AUTOPILOT.overshootFrom) {\n      await glide(r, to, ms, { signal, bow: AUTOPILOT.bow, onFrame: step });\n      return;\n    }\n    // Long trips land a little past the target, then correct.\n    const by = Math.min(distance * AUTOPILOT.overshoot, AUTOPILOT.overshootMax);\n    const ux = (to.x - from.x) / distance;\n    const uy = (to.y - from.y) / distance;\n    const drift = (Math.random() - 0.5) * by * 0.8;\n    const past = { x: to.x + ux * by - uy * drift, y: to.y + uy * by + ux * drift };\n    await glide(r, past, ms - AUTOPILOT.correct / 2, { signal, bow: AUTOPILOT.bow, onFrame: step });\n    await glide(r, to, AUTOPILOT.correct, { signal, ease: EASE_OUT, onFrame: step });\n  };\n\n  /** Goes to a target, unless the tip is already on it. Catches up if it moved on the way. */\n  const approach = async (r: Rig, element: Element, at: [number, number] | undefined, signal: AbortSignal) => {\n    if (!at && inside(boxOf(element), tip(r))) return;\n    await moveTo(r, aim(element, at), signal);\n    if (!inside(boxOf(element), tip(r), 0)) await moveTo(r, aim(element, at), signal);\n  };\n\n  const squash = (r: Rig, down: boolean) => {\n    if (down) {\n      animate(r.squash, AUTOPILOT.squash, { duration: 0.07, ease: \"easeOut\" });\n      if (!config.reduced) {\n        r.ring.jump(0);\n        animate(r.ring, 1, { duration: AUTOPILOT.ripple, ease: EASE_OUT });\n      }\n    } else {\n      animate(\n        r.squash,\n        1,\n        config.reduced ? { duration: 0.1 } : { type: \"spring\", stiffness: 600, damping: 18 },\n      );\n    }\n  };\n\n  /** Presses and releases where the tip is. Once started it always finishes. */\n  const click = async (r: Rig, target: Element) => {\n    const p = tip(r);\n    const at = toClient(p);\n    const under = hit(p);\n    const on = under && target.contains(under) ? under : target;\n    acted();\n    flag(target, \"press\", true);\n    squash(r, true);\n    fire(on, \"pointerdown\", at);\n    fire(on, \"mousedown\", at);\n    await sleep(AUTOPILOT.press);\n    flag(target, \"press\", false);\n    squash(r, false);\n    acted();\n    fire(on, \"pointerup\", at);\n    fire(on, \"mouseup\", at);\n    fire(on, \"click\", at);\n    // A click on a field shows it focused; anywhere else clears it.\n    focusOn(r, on.closest(\"input, textarea\"));\n  };\n\n  const typeInto = async (r: Rig, step: { type: string; into: string; at?: [number, number] }, signal: AbortSignal) => {\n    const target = await find(step.into, signal);\n    if (!target) return;\n    const field = fieldIn(target);\n    if (!field) return warn(`\"${step.into}\" has no input or textarea to type into.`);\n    if (r.focus !== field) {\n      const box = field.getClientRects().length ? field : target;\n      await approach(r, box, step.at, signal);\n      await sleep(vary(AUTOPILOT.beat, 0.3), signal);\n      await click(r, box);\n    }\n    focusOn(r, field);\n    if (!config.reduced) {\n      // Ease the pointer off the line, so the words show as they're typed.\n      const p = tip(r);\n      const below = boxOf(field).bottom + 4;\n      glide(\n        r,\n        { x: p.x + 10, y: clamp(Math.max(p.y + 6, below), 0, height.get() - 40) },\n        280,\n        { ease: EASE_OUT },\n      ).catch(noop);\n    }\n    await sleep(AUTOPILOT.key * 2, signal);\n    const keys = keysOf(step.type);\n    let burst = Math.random() < AUTOPILOT.burst ? 3 : 0;\n    for (let i = 0; i < keys.length; i++) {\n      const key = keys[i];\n      press(field, key);\n      if (i === keys.length - 1) break;\n      let gap = vary(AUTOPILOT.key, 0.45);\n      if (burst > 0) {\n        burst--;\n        gap *= 0.4;\n      }\n      if (key === \" \") {\n        gap += vary(AUTOPILOT.space, 0.5);\n        if (Math.random() < AUTOPILOT.burst) burst = 2 + Math.floor(Math.random() * 3);\n      } else if (/[.,!?;:…]/.test(key)) {\n        gap += vary(AUTOPILOT.stop, 0.4);\n      }\n      await sleep(gap, signal);\n    }\n  };\n\n  const drag = async (\n    r: Rig,\n    step: { drag: string; to: string; drop?: () => void; at?: [number, number] },\n    signal: AbortSignal,\n  ) => {\n    const item = await find(step.drag, signal);\n    const zone = item && (await find(step.to, signal));\n    if (!(item instanceof HTMLElement) || !zone) return;\n    await approach(r, item, step.at, signal);\n    await sleep(AUTOPILOT.beat, signal);\n\n    // Carry the grab point so the item's middle lands on the drop target's.\n    const grab = tip(r);\n    const from = boxOf(item);\n    const onto = boxOf(zone);\n    const end = {\n      x: onto.left + onto.width / 2 + (grab.x - (from.left + from.width / 2)),\n      y: onto.top + onto.height / 2 + (grab.y - (from.top + from.height / 2)),\n    };\n\n    const held = lift(item);\n    flag(item, \"drag\", true);\n    squash(r, true);\n    if (!config.reduced) await animate(0, 1, { duration: 0.14, ease: \"easeOut\", onUpdate: held.raise });\n    // The drop target shows data-autopilot-drop while the item is over it.\n    let over = false;\n    const hovering = (p: Point) => {\n      const under = hit(p);\n      const now = !!under && zone.contains(under);\n      if (now !== over) flag(zone, \"drop\", (over = now));\n    };\n    try {\n      await moveTo(r, end, signal, {\n        carry: true,\n        onFrame: (p) => {\n          held.move(p.x - grab.x, p.y - grab.y);\n          hovering(p);\n        },\n      });\n      hovering(tip(r));\n      await sleep(AUTOPILOT.beat, signal);\n    } catch (error) {\n      // Taken over mid-drag: put it back.\n      if (over) flag(zone, \"drop\", false);\n      squash(r, false);\n      const p = tip(r);\n      const back = { x: p.x - grab.x, y: p.y - grab.y };\n      animate(1, 0, {\n        ...AUTOPILOT.settle,\n        onUpdate: (v) => {\n          held.move(back.x * v, back.y * v);\n          held.raise(v);\n        },\n        onComplete: () => {\n          held.restore();\n          flag(item, \"drag\", false);\n        },\n      });\n      throw error;\n    }\n\n    // Set it down: let the UI move it, then settle it from where it was dropped into its new place.\n    if (over) flag(zone, \"drop\", false);\n    squash(r, false);\n    const dropped = item.getBoundingClientRect();\n    step.drop?.();\n    await nextFrame();\n    await nextFrame();\n    const placed = content?.querySelector<HTMLElement>(`[data-autopilot=\"${CSS.escape(step.drag)}\"]`);\n    let settling = held;\n    if (placed && placed !== item) {\n      held.restore();\n      flag(item, \"drag\", false);\n      flag(placed, \"drag\", true);\n      settling = lift(placed);\n      settling.raise(1);\n    }\n    const target = settling.element;\n    settling.move(0, 0);\n    const now = target.getBoundingClientRect();\n    const f = frame();\n    const offset = {\n      x: (dropped.left + dropped.width / 2 - (now.left + now.width / 2)) / f.k,\n      y: (dropped.top + dropped.height / 2 - (now.top + now.height / 2)) / f.k,\n    };\n    const finish = () => {\n      settling.restore();\n      flag(target, \"drag\", false);\n    };\n    if (config.reduced) return finish();\n    settling.move(offset.x, offset.y);\n    animate(1, 0, {\n      ...AUTOPILOT.settle,\n      onUpdate: (v) => {\n        settling.move(offset.x * v, offset.y * v);\n        settling.raise(v);\n      },\n      onComplete: finish,\n    });\n  };\n\n  /** One cursor's steps in a chapter, in order. */\n  const walk = async (id: string, steps: AutopilotStep[], signal: AbortSignal) => {\n    const r = rig(id);\n    for (const step of steps) {\n      if (signal.aborted) throw signal.reason;\n      if (step.say !== undefined) ui.onSay(id, step.say);\n      if (\"move\" in step) {\n        const target = await find(step.move, signal);\n        if (target) await approach(r, target, step.at, signal);\n      } else if (\"click\" in step) {\n        const target = await find(step.click, signal);\n        if (!target) continue;\n        await approach(r, target, step.at, signal);\n        await sleep(vary(AUTOPILOT.beat, 0.3), signal);\n        await click(r, target);\n      } else if (\"type\" in step) {\n        await typeInto(r, step, signal);\n      } else if (\"drag\" in step) {\n        await drag(r, step, signal);\n      } else if (\"wait\" in step) {\n        await sleep(step.wait, signal);\n      } else if (\"do\" in step) {\n        await step.do();\n      }\n    }\n  };\n\n  const estimate = (tracks: AutopilotStep[][]) =>\n    Math.max(\n      1000,\n      ...tracks.map((steps) =>\n        steps.reduce(\n          (ms, step) =>\n            ms +\n            (\"wait\" in step\n              ? step.wait\n              : \"type\" in step\n                ? 900 + step.type.length * 95\n                : \"drag\" in step\n                  ? 2000\n                  : \"click\" in step\n                    ? 1000\n                    : \"move\" in step\n                      ? 700\n                      : 0),\n          0,\n        ),\n      ),\n    );\n\n  /** Plays a chapter: every cursor's steps at once, each cursor's in order. */\n  const play = async (index: number, signal: AbortSignal) => {\n    const lead = config.cursors[0]?.id;\n    const tracks = new Map<string, AutopilotStep[]>();\n    for (const step of config.chapters[index].steps) {\n      const id = step.cursor ?? lead;\n      if (!id) continue;\n      if (!config.cursors.some((cursor) => cursor.id === id)) {\n        warn(`no cursor with id \"${id}\".`);\n        continue;\n      }\n      tracks.set(id, [...(tracks.get(id) ?? []), step]);\n    }\n    fill?.stop();\n    progress.jump(0);\n    const expected = measured[index] ?? estimate([...tracks.values()]);\n    fill = animate(progress, 0.96, { duration: expected / 1000, ease: \"linear\" });\n    const began = performance.now();\n    const results = await Promise.allSettled(\n      [...tracks].map(([id, steps]) => walk(id, steps, signal)),\n    );\n    if (signal.aborted) throw signal.reason;\n    for (const result of results) if (result.status === \"rejected\") throw result.reason;\n    measured[index] = performance.now() - began;\n    fill.stop();\n    fill = animate(progress, 1, { duration: 0.2, ease: \"easeOut\" });\n  };\n\n  /** Crossfades from the stage as it is to the chapter's setup. */\n  const reset = async (index: number, signal: AbortSignal) => {\n    const copy = content && host ? snapshot(content, host) : null;\n    try {\n      unflagAll();\n      config.chapters[index].setup?.();\n      await nextFrame(signal);\n      if (copy) await animate(copy, { opacity: [1, 0] }, { duration: AUTOPILOT.fade, ease: \"easeOut\" });\n    } finally {\n      copy?.remove();\n    }\n  };\n\n  const goHome = (signal: AbortSignal) =>\n    Promise.all(\n      crew().map(async (r, i) => {\n        await moveTo(r, home(i), signal);\n        r.placed = \"home\";\n      }),\n    );\n\n  const park = async () => {\n    parked = true;\n    ui.onParked(true);\n    unflagAll();\n    await Promise.all(\n      crew().map(async (r, i) => {\n        r.placed = \"park\";\n        animate(r.rest, 1, { duration: 0.3 });\n        if (!config.reduced) return glide(r, spot(i), AUTOPILOT.yield, { bow: AUTOPILOT.bow, ease: EASE_OUT });\n        r.stop();\n        r.x.jump(spot(i).x);\n        r.y.jump(spot(i).y);\n      }),\n    );\n  };\n\n  const unpark = () => {\n    parked = false;\n    ui.onParked(false);\n    for (const r of crew()) animate(r.rest, 0, { duration: 0.25 });\n  };\n\n  const run = async (signal: AbortSignal) => {\n    if (signal.aborted || !stage) return;\n    try {\n      if (parked) unpark();\n      if (pending !== null) {\n        chapter = pending;\n        pending = null;\n        ended = false;\n      }\n      for (;;) {\n        const count = config.chapters.length;\n        if (!count) return;\n        if (ended || chapter >= count) {\n          ended = false;\n          chapter = 0;\n          ui.onQuiet();\n          await Promise.all([reset(0, signal), goHome(signal)]);\n        } else if (dirty) {\n          await reset(chapter, signal);\n        } else {\n          config.chapters[chapter].setup?.();\n          await nextFrame(signal);\n        }\n        dirty = true;\n        const index = chapter;\n        ui.onChapter(index);\n        config.onChapterChange?.(index);\n        ui.onQuiet();\n        await play(index, signal);\n        chapter = index + 1;\n        if (chapter >= config.chapters.length) {\n          ended = true;\n          if (!config.loop || config.reduced) return ui.onFinish();\n          await sleep(AUTOPILOT.hold, signal);\n        }\n      }\n    } catch (error) {\n      if (!signal.aborted) throw error;\n      fill?.stop();\n      if (signal.reason === \"yield\") await park();\n    }\n  };\n\n  /** Puts cursors that haven't been seen yet at home and fades them in. */\n  const show = () => {\n    if (!stage) return;\n    config.cursors.forEach((cursor, i) => {\n      const r = rig(cursor.id);\n      if (r.placed || r.opacity.get() > 0) return;\n      r.x.jump(home(i).x);\n      r.y.jump(home(i).y);\n      r.placed = \"home\";\n      if (config.reduced) r.opacity.jump(1);\n      else animate(r.opacity, 1, { duration: 0.4, ease: \"easeOut\" });\n    });\n  };\n\n  return {\n    progress,\n    width,\n    height,\n    rig,\n    configure(next: PilotConfig) {\n      config = next;\n      show();\n    },\n    listen(next: PilotUI) {\n      ui = next;\n      return () => {\n        ui = { onChapter: noop, onSay: noop, onQuiet: noop, onParked: noop, onFinish: noop };\n      };\n    },\n    connect(nextStage: HTMLElement | null, nextContent: HTMLElement | null, nextHost: HTMLElement | null) {\n      if (!nextStage || !nextContent || !nextHost) return;\n      stage = nextStage;\n      content = nextContent;\n      host = nextHost;\n      const measure = () => {\n        width.set(nextStage.clientWidth);\n        height.set(nextStage.clientHeight);\n        config.cursors.forEach((cursor, i) => {\n          const r = rig(cursor.id);\n          const at = r.placed === \"home\" ? home(i) : r.placed === \"park\" ? spot(i) : null;\n          if (!at) return;\n          r.stop();\n          r.x.jump(at.x);\n          r.y.jump(at.y);\n        });\n      };\n      measure();\n      show();\n      const observer = new ResizeObserver(measure);\n      observer.observe(nextStage);\n      return () => {\n        observer.disconnect();\n        unflagAll();\n        stage = content = host = null;\n      };\n    },\n    /** Starts a run once the one before it has finished the action it was in. */\n    start(signal: AbortSignal) {\n      queue = queue\n        .then(() => run(signal))\n        .catch((error) => {\n          if (!signal.aborted) console.error(error);\n        });\n    },\n    /** Why a run is being stopped: the visitor taking over, or anything else. */\n    halt: () => (config.yielding ? \"yield\" : \"stop\"),\n    seek(index: number) {\n      pending = index;\n      dirty = true;\n      fill?.stop();\n      progress.jump(0);\n    },\n    /** Whether the UI is reacting to something a cursor just did, like a menu focusing itself. */\n    acting: () => performance.now() < actingUntil,\n  };\n}\n\ntype Pilot = ReturnType<typeof createPilot>;\n\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(REDUCED);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\n\n/** Reduced motion, read so the server and the first client render agree. */\nfunction useReduced() {\n  return React.useSyncExternalStore(\n    subscribeReduced,\n    () => window.matchMedia(REDUCED).matches,\n    () => false,\n  );\n}\n\n/* ── Autopilot ──────────────────────────────────────────── */\n\nexport interface AutopilotProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** The cursors on the stage. Steps without a `cursor` belong to the first. */\n  cursors: AutopilotCursor[];\n  /** The script, in chapters. Each starts from its `setup`, so a takeover can replay it. */\n  chapters: AutopilotChapter[];\n  /** The UI the cursors use. Mark targets with `data-autopilot=\"<name>\"`. */\n  children: React.ReactNode;\n  /** Show the chapters under the stage, filling like a story timer. Click one to jump to it. */\n  chapterStrip?: boolean;\n  /** Start over after the last chapter. Otherwise it stops on the finished state. */\n  loop?: boolean;\n  /** The parked cursor's label while the visitor has control. */\n  yieldLabel?: string;\n  /** The corner the cursors step aside to. Pick one your UI keeps clear. */\n  park?: AutopilotCorner;\n  /** Called when a chapter starts. */\n  onChapterChange?: (index: number) => void;\n  /** Classes for the stage, the box the cursors move in. */\n  stageClassName?: string;\n}\n\n/**\n * A cursor that uses your UI on its own: it moves, clicks, types and drags\n * across real markup on a script, and steps aside when a visitor wants to try it.\n */\nexport function Autopilot({\n  cursors,\n  chapters,\n  children,\n  chapterStrip = false,\n  loop = true,\n  yieldLabel = \"Your turn\",\n  park = \"bottom-right\",\n  onChapterChange,\n  stageClassName,\n  className,\n  ...props\n}: AutopilotProps) {\n  const reduced = useReduced();\n  const [pilot] = React.useState(createPilot);\n  const stageRef = React.useRef<HTMLDivElement>(null);\n  const contentRef = React.useRef<HTMLDivElement>(null);\n  const hostRef = React.useRef<HTMLDivElement>(null);\n\n  const [chapter, setChapter] = React.useState(0);\n  const [says, setSays] = React.useState<Record<string, string | null>>({});\n  const [parked, setParked] = React.useState(false);\n  const [intent, setIntent] = React.useState<boolean | null>(null); // the visitor's play or pause, once pressed\n  const [hovered, setHovered] = React.useState(false);\n  const [focused, setFocused] = React.useState(false);\n  const [touched, setTouched] = React.useState(false);\n  const [seen, setSeen] = React.useState(false);\n  const [jump, setJump] = React.useState(0);\n\n  const playing = intent ?? !reduced;\n  const yielded = hovered || focused || touched || !playing;\n  const running = seen && !yielded;\n\n  React.useLayoutEffect(() => {\n    pilot.configure({ cursors, chapters, reduced, loop, park, yielding: yielded, onChapterChange });\n  });\n\n  React.useLayoutEffect(\n    () =>\n      pilot.listen({\n        onChapter: setChapter,\n        onSay: (id, text) => setSays((current) => ({ ...current, [id]: text })),\n        onQuiet: () => setSays({}),\n        onParked: setParked,\n        onFinish: () => setIntent(false),\n      }),\n    [pilot],\n  );\n\n  React.useLayoutEffect(\n    () => pilot.connect(stageRef.current, contentRef.current, hostRef.current),\n    [pilot],\n  );\n\n  React.useEffect(() => {\n    if (!running) return;\n    const run = new AbortController();\n    pilot.start(run.signal);\n    return () => run.abort(pilot.halt());\n  }, [pilot, running, jump]);\n\n  // Off screen or in a background tab, it stops, and replays the chapter when it's back.\n  React.useEffect(() => {\n    const stage = stageRef.current;\n    if (!stage) return;\n    let inView = false;\n    const update = () => setSeen(inView && document.visibilityState === \"visible\");\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { threshold: 0.2 },\n    );\n    observer.observe(stage);\n    document.addEventListener(\"visibilitychange\", update);\n    return () => {\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", update);\n    };\n  }, []);\n\n  /* The visitor: a mouse inside or focus inside holds it until two seconds after they leave; a tap holds it until Resume. */\n  const timers = React.useRef({ hover: 0, focus: 0, pointer: \"mouse\" });\n  React.useEffect(() => {\n    const t = timers.current;\n    return () => {\n      window.clearTimeout(t.hover);\n      window.clearTimeout(t.focus);\n    };\n  }, []);\n\n  const holdHover = () => {\n    window.clearTimeout(timers.current.hover);\n    setHovered(true);\n  };\n  const letGo = (kind: \"hover\" | \"focus\") => {\n    const t = timers.current;\n    window.clearTimeout(t[kind]);\n    t[kind] = window.setTimeout(\n      () => (kind === \"hover\" ? setHovered(false) : setFocused(false)),\n      AUTOPILOT.resume,\n    );\n  };\n\n  const resume = () => {\n    const t = timers.current;\n    window.clearTimeout(t.hover);\n    window.clearTimeout(t.focus);\n    setHovered(false);\n    setFocused(false);\n    setTouched(false);\n    setIntent(true);\n    const active = document.activeElement;\n    if (active instanceof HTMLElement && stageRef.current?.contains(active)) active.blur();\n  };\n\n  const seek = (index: number) => {\n    pilot.seek(index);\n    setChapter(index);\n    resume();\n    setJump((n) => n + 1);\n  };\n\n  const control: ControlState = touched ? \"resume\" : playing ? \"pause\" : \"play\";\n  const onControl = () => (control === \"pause\" ? setIntent(false) : resume());\n\n  return (\n    <div {...props} className={cn(\"relative\", className)}>\n      <div className=\"relative\">\n        <div\n          ref={stageRef}\n          className={cn(\"relative\", stageClassName)}\n          onPointerEnter={(event) => {\n            if (event.nativeEvent.isTrusted && event.pointerType === \"mouse\") holdHover();\n          }}\n          onPointerMove={(event) => {\n            if (event.nativeEvent.isTrusted && event.pointerType === \"mouse\" && !hovered) holdHover();\n          }}\n          onPointerLeave={(event) => {\n            if (event.nativeEvent.isTrusted && event.pointerType === \"mouse\") letGo(\"hover\");\n          }}\n          onPointerDown={(event) => {\n            if (event.nativeEvent.isTrusted) timers.current.pointer = event.pointerType;\n          }}\n          onClickCapture={(event) => {\n            if (event.nativeEvent.isTrusted && timers.current.pointer !== \"mouse\") setTouched(true);\n          }}\n          onFocus={() => {\n            if (pilot.acting()) return;\n            window.clearTimeout(timers.current.focus);\n            setFocused(true);\n          }}\n          onBlur={(event) => {\n            if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n            letGo(\"focus\");\n          }}\n        >\n          <div ref={contentRef} className=\"relative isolate h-full\">\n            {children}\n          </div>\n          <div ref={hostRef} aria-hidden className=\"pointer-events-none absolute inset-0 z-10\" />\n          <div aria-hidden className=\"pointer-events-none absolute inset-0 z-20\">\n            {cursors.map((cursor, i) => {\n              const said = says[cursor.id];\n              const text = parked ? (i === 0 ? yieldLabel : null) : said === undefined ? cursor.label : (said ?? cursor.label);\n              return (\n                <Pointer\n                  key={cursor.id}\n                  id={cursor.id}\n                  rig={pilot.rig(cursor.id)}\n                  pilot={pilot}\n                  color={cursor.color}\n                  text={text || null}\n                  reduced={reduced}\n                />\n              );\n            })}\n          </div>\n        </div>\n        {!chapterStrip && (\n          <Control\n            state={control}\n            onClick={onControl}\n            className=\"absolute bottom-3 left-3 z-30 bg-zinc-950/80 text-white shadow-sm ring-1 ring-white/10 backdrop-blur-sm hover:bg-zinc-950 dark:bg-white/85 dark:text-zinc-900 dark:ring-black/5 dark:hover:bg-white\"\n          />\n        )}\n      </div>\n      {chapterStrip && (\n        <div className=\"mt-4 flex items-start gap-3\" role=\"group\" aria-label=\"Chapters\">\n          <Control\n            state={control}\n            onClick={onControl}\n            className=\"mt-0.5 shrink-0 ring-1 ring-current/15 hover:bg-current/5\"\n          />\n          <ol\n            className=\"grid min-w-0 flex-1 gap-2 sm:gap-3\"\n            style={{ gridTemplateColumns: `repeat(${chapters.length}, minmax(0, 1fr))` }}\n          >\n            {chapters.map((item, i) => (\n              <li key={i} className=\"min-w-0\">\n                <button\n                  type=\"button\"\n                  onClick={() => seek(i)}\n                  aria-current={i === chapter ? \"step\" : undefined}\n                  className=\"group/chapter block w-full rounded-sm py-1.5 text-left outline-current focus-visible:outline-2 focus-visible:outline-offset-2\"\n                >\n                  <span className=\"block h-[3px] overflow-hidden rounded-full bg-current/15\">\n                    <motion.span\n                      className=\"block h-full origin-left rounded-full bg-current\"\n                      style={{ scaleX: i < chapter ? 1 : i === chapter ? pilot.progress : 0 }}\n                    />\n                  </span>\n                  <span\n                    className={cn(\n                      \"mt-2 flex items-baseline gap-1.5 text-xs transition-opacity duration-200\",\n                      i === chapter ? \"opacity-100\" : \"opacity-50 group-hover/chapter:opacity-80\",\n                    )}\n                  >\n                    <span className=\"font-mono tabular-nums opacity-60\">\n                      {String(i + 1).padStart(2, \"0\")}\n                    </span>\n                    <span className=\"truncate font-medium\">{item.title}</span>\n                  </span>\n                </button>\n              </li>\n            ))}\n          </ol>\n        </div>\n      )}\n    </div>\n  );\n}\n\n/* ── Parts ──────────────────────────────────────────────── */\n\nfunction Pointer({\n  id,\n  rig,\n  pilot,\n  color,\n  text,\n  reduced,\n}: {\n  id: string;\n  rig: Rig;\n  pilot: Pilot;\n  color: string;\n  text: string | null;\n  reduced: boolean;\n}) {\n  // Keep the last words while the pill fades out, so it doesn't empty first.\n  const [kept, setKept] = React.useState(text);\n  if (text !== null && text !== kept) setKept(text);\n  const shown = text ?? kept;\n\n  const measureRef = React.useRef<HTMLSpanElement>(null);\n  const pill = useMotionValue(0);\n  React.useEffect(() => {\n    const element = measureRef.current;\n    if (!element) return;\n    let first = true;\n    const observer = new ResizeObserver(() => {\n      const w = element.offsetWidth;\n      if (!w) return;\n      if (first || reduced) pill.jump(w);\n      else animate(pill, w, AUTOPILOT.label);\n      first = false;\n    });\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [pill, reduced]);\n\n  // Slides along the stage's edge instead of hanging off it, and moves above the tip near the bottom.\n  const left = useTransform(() => {\n    const x = rig.x.get();\n    const max = pilot.width.get() - pill.get() - PILL.inset;\n    return Math.max(PILL.inset, Math.min(x + PILL.x, max)) - x;\n  });\n  const flip = useTransform((): number =>\n    rig.y.get() + PILL.y + PILL.height + PILL.inset > pilot.height.get() ? 1 : 0,\n  );\n  const flipped = useSpring(flip, { stiffness: 500, damping: 40 });\n  const top = useTransform(() => {\n    const f = reduced ? flip.get() : flipped.get();\n    return PILL.y + f * (-PILL.height - 8 - PILL.y);\n  });\n\n  const arrowOpacity = useTransform(rig.rest, [0, 1], [1, AUTOPILOT.parked]);\n  const ringScale = useTransform(rig.ring, [0, 1], [0.25, 1]);\n  const ringOpacity = useTransform(rig.ring, [0, 0.15, 1], [0, 0.6, 0]);\n\n  return (\n    <motion.div\n      data-autopilot-cursor={id}\n      className=\"absolute top-0 left-0 will-change-transform\"\n      style={{ x: rig.x, y: rig.y, opacity: rig.opacity }}\n    >\n      <motion.span\n        className=\"absolute -top-4 -left-4 size-8 rounded-full border-2\"\n        style={{ borderColor: color, scale: ringScale, opacity: ringOpacity }}\n      />\n      <motion.span\n        className=\"absolute -top-[2px] -left-[2px] block origin-[2px_2px]\"\n        style={{\n          scale: rig.squash,\n          opacity: arrowOpacity,\n          filter: \"drop-shadow(0 1px 1.5px rgb(0 0 0 / 0.3)) drop-shadow(0 4px 8px rgb(0 0 0 / 0.12))\",\n        }}\n      >\n        <svg width=\"24\" height=\"26\" viewBox=\"0 0 24 26\" fill=\"none\" className=\"block overflow-visible\">\n          <path\n            d=\"M2 2 L2 21.8 L7.6 16.9 L16.6 16.5 Z\"\n            fill={color}\n            stroke=\"white\"\n            strokeWidth=\"1.5\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </motion.span>\n      <motion.div\n        className=\"absolute top-0 left-0 h-6 origin-top-left overflow-hidden rounded-full shadow-sm\"\n        style={{ x: left, y: top, width: pill, backgroundColor: color }}\n        initial={false}\n        animate={{ opacity: text ? 1 : 0, scale: text ? 1 : 0.85 }}\n        transition={{ duration: 0.2, ease: \"easeOut\" }}\n      >\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={shown}\n            className=\"absolute inset-y-0 left-0 flex items-center px-2.5 text-xs font-medium whitespace-nowrap text-white\"\n            initial={reduced ? { opacity: 0 } : { y: \"100%\", opacity: 0 }}\n            animate={{ y: \"0%\", opacity: 1 }}\n            exit={reduced ? { opacity: 0 } : { y: \"-100%\", opacity: 0 }}\n            transition={reduced ? { duration: 0.15 } : AUTOPILOT.label}\n          >\n            {shown}\n          </motion.span>\n        </AnimatePresence>\n      </motion.div>\n      <span\n        ref={measureRef}\n        className=\"invisible absolute top-0 left-0 px-2.5 text-xs font-medium whitespace-nowrap\"\n      >\n        {shown}\n      </span>\n    </motion.div>\n  );\n}\n\ntype ControlState = \"pause\" | \"play\" | \"resume\";\n\nfunction Control({\n  state,\n  onClick,\n  className,\n}: {\n  state: ControlState;\n  onClick: () => void;\n  className?: string;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={cn(\n        \"inline-flex h-7 min-w-7 items-center justify-center gap-1.5 rounded-full text-xs font-medium transition-colors outline-current focus-visible:outline-2 focus-visible:outline-offset-2\",\n        state === \"pause\" ? \"px-0\" : \"pr-3 pl-2.5\",\n        className,\n      )}\n    >\n      {state === \"pause\" ? (\n        <svg viewBox=\"0 0 12 12\" className=\"size-3\" fill=\"currentColor\" aria-hidden>\n          <rect x=\"2.5\" y=\"2\" width=\"2.5\" height=\"8\" rx=\"0.75\" />\n          <rect x=\"7\" y=\"2\" width=\"2.5\" height=\"8\" rx=\"0.75\" />\n        </svg>\n      ) : (\n        <svg viewBox=\"0 0 12 12\" className=\"size-3\" fill=\"currentColor\" aria-hidden>\n          <path d=\"M3.5 2.2v7.6a.6.6 0 0 0 .9.5l6-3.8a.6.6 0 0 0 0-1L4.4 1.7a.6.6 0 0 0-.9.5Z\" />\n        </svg>\n      )}\n      <span className={state === \"pause\" ? \"sr-only\" : undefined}>\n        {state === \"pause\" ? \"Pause\" : state === \"resume\" ? \"Resume\" : \"Play\"}\n      </span>\n    </button>\n  );\n}\n"
    }
  ]
}