{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reroute",
  "type": "registry:ui",
  "title": "Reroute",
  "description": "Your hub at the top, wired by a cable to one option on a wheel of many. Requests run down the cable into the live option. Pick another, spin the wheel or arrow to it, and the cable lifts out, the wheel turns it round to the socket and the cable drops in with a click. When the live option goes down, its request bounces back up the cable and the wheel turns on to the next healthy one, then back again once it recovers.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "interaction",
    "ai",
    "diagram"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/reroute",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/reroute.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - Every moving part animates by transform and opacity only, through WAAPI and inline styles the\n *   engine writes; React draws the first layout in `cqw` and never re-renders per frame.\n * - A turn's keyframes are sampled round the rim, and a tile crossing the far side gets two hidden\n *   frames at one offset, so nothing sweeps back across the wheel.\n * - The JS `bezier` curves match the WAAPI easings, so a new pick takes the wheel up mid-flight\n *   from exactly where it is.\n * - The cable lifts before the wheel turns and lowers `drop.lead` before the turn ends, following\n *   the new option in; comets run only while it's seated, on screen and on a visible tab.\n * - Under reduced motion there's no turning, cable or comets: the wheel jumps and the name\n *   cross-fades. The options stay a `radiogroup` whose live option holds the only tab stop.\n */\nconst TIMING = {\n  /** How fast a reroute plays, as a multiple of the pace below (the `speed` prop). */\n  speed: 1,\n  // At the default pace; `speed` divides everything from here down to `dim`.\n  /** The wheel turning, in ms: a one-step turn, more per extra step, and the cap. */\n  turn: { base: 480, per: 120, max: 840 },\n  /** A let-go wheel coming to rest, in ms. */\n  settle: 380,\n  /** After the cable starts to lift, before the wheel turns, in ms. */\n  turnLag: 40,\n  /** Before the turn ends, when the name rises in, in ms. */\n  labelLead: 40,\n  /** The cable pulling up into the hub, and the socket dimming with it, in ms. */\n  lift: 150,\n  /**\n   * The cable lowering back in: ms it takes; ms before the turn ends that it starts, so it follows\n   * the option in; share of the way through when it touches down.\n   */\n  drop: { duration: 440, lead: 300, seat: 0.82 },\n  /** Once seated, in ms: the socket ring settling, and a pulse ringing out. */\n  click: 320,\n  pulse: 460,\n  /** A failed request running back up, in ms, and ms before the wheel moves. */\n  bounce: { duration: 260, lead: 200 },\n  /** The socket flashing on a failure, in ms. */\n  fail: 360,\n  /** The name going out and coming in, in ms. */\n  label: { out: 120, in: 240 },\n  /** The name dimming when every option is down, in ms. */\n  dim: 200,\n  // Not paced.\n  /** The name cross-fading under reduced motion, in ms. */\n  crossFade: 160,\n  /** Ms each comet takes to run down the cable (the `flowDuration` prop). */\n  flowDuration: 1200,\n  /** Between turns with `autoplay`, in ms. */\n  autoplay: 2800,\n  /** Each way of the hub's glow breathing, in ms, and its curve. */\n  glow: { duration: 1800, easing: \"ease-in-out\" },\n  /** Drag: ms of momentum on let-go; ms of motion read for speed. */\n  drag: { carry: 140, window: 90 },\n  /** After a drag, before a controlled wheel that didn't take the pick goes back, in ms. */\n  revert: 60,\n  /** Curves, as CSS easings. */\n  inOut: \"cubic-bezier(0.65, 0, 0.35, 1)\", // turning: picks up and puts down with weight\n  easeOut: \"cubic-bezier(0.23, 1, 0.32, 1)\", // settling, lifting, labels\n  lower: \"cubic-bezier(0.65, 0, 0.35, 1)\", // the cable lowering: picks up slowly and lands soft, no snap\n  /** `inOut` and `easeOut` as control points, to pick a turn up mid-flight. Keep them in step. */\n  inOutCurve: [0.65, 0, 0.35, 1],\n  easeOutCurve: [0.23, 1, 0.32, 1],\n  /** The socket and the name fading out, as a CSS easing. */\n  fade: \"ease-out\",\n  /** The comets fading out and in, as a Tailwind class. */\n  flowFade: \"duration-300\",\n  /** A tile's border on hover, as a Tailwind class. */\n  hover: \"duration-150\",\n  /** A tile's icon dimming and its down dot showing, as a Tailwind class. */\n  down: \"duration-200\",\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * REROUTE STORYBOARD\n *\n *  rest      your hub tile sits at the top, lit from behind. A cable\n *            runs down from it into a socket at the top of a wheel,\n *            and the live option sits in the socket. The others ride\n *            the rim either side, smaller (0.86, 0.72) and fainter\n *            the further round they are, and the rest wait out of\n *            sight past the edges (opacity 0.8, 0.5, then gone). Its name and detail sit under\n *            the socket\n *  flow      requests run down the cable as small comets, three at a\n *            time, 1.2s each, into the live option. They stop off\n *            screen and on a hidden tab\n *  pick      press an option (or arrow to it) and the cable lifts\n *            up into the hub (45% of its length, 150ms), the wheel turns\n *            the short way, every option gliding round the rim to\n *            its new place (480ms, +120ms a step, at most 840ms,\n *            in-out). 300ms before the turn ends the cable starts to\n *            lower back down, following the new one in (440ms, in-out:\n *            it picks up slowly and lands soft); once it's seated the socket ring\n *            settles from 1.08, a pulse rings out and the comets\n *            fade back in. The name under it rises in 6px\n *  down      an option in `down` dims and wears a dot. If it was\n *            live, its request bounces back up the cable and the\n *            socket flashes, then the wheel turns forward to the\n *            next healthy option, as above, ~200ms later\n *  recover   when the option you picked comes back, the wheel turns\n *            back to it. With every option down the cable waits up in\n *            the hub and the name dims to 0.4; the first one back gets\n *            the cable lowered into it\n *  drag      drag sideways on the wheel to spin it by hand, one step\n *            per ~arc gap; let go and it carries on with your speed\n *            for a moment, then settles on the nearest healthy\n *            option (380ms ease-out). A flick spins it further\n *  autoplay  turns to the next healthy option every 2.8s; it pauses\n *            under the pointer, while focused and off screen\n *  keys      the options are a radio group: Tab lands on the live\n *            one, ←/→ (or ↑/↓) pick the previous or next healthy one\n *  touch     a tap picks; a sideways swipe spins; vertical swipes\n *            scroll the page\n *  resize    the wheel is measured from the box: its radius is 0.72×\n *            the width, so the rim curves the same at any size\n *  reduced   no turning, no cable, no comets: the wheel jumps to the\n *            new option and the name cross-fades (160ms). Dragging\n *            still spins it, without the carry\n * ───────────────────────────────────────────────────────── */\n\nconst WHEEL = {\n  radius: 0.72, // the wheel's radius, as a share of the box width\n  step: 18.5, // degrees between options round the rim: neighbours at ±0.23 of the width, the next ones fading at ±0.43\n  shrink: 0.14, // scale lost per step away from the socket\n  shown: 2.6, // steps from the socket before an option is fully out of sight\n  lift: { by: 45 }, // % of its length the cable pulls up into the hub\n  click: { from: 1.08, pulse: 1.28 }, // once seated: the socket ring settles from 1.08 and a pulse rings out to 1.28\n  label: { rise: 6 }, // px\n  flow: { count: 3 }, // comets on the cable\n  drag: { slop: 6 }, // px before a press becomes a drag\n} as const;\n\nconst REDUCED = \"(prefers-reduced-motion: reduce)\";\n\n/** Evaluates a CSS cubic-bezier, so a turn can be picked up mid-flight from exactly where it is. */\nfunction bezier(x1: number, y1: number, x2: number, y2: number) {\n  const cx = 3 * x1;\n  const bx = 3 * (x2 - x1) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * y1;\n  const by = 3 * (y2 - y1) - cy;\n  const ay = 1 - cy - by;\n  const sx = (t: number) => ((ax * t + bx) * t + cx) * t;\n  const dx = (t: number) => (3 * ax * t + 2 * bx) * t + cx;\n  return (x: number) => {\n    if (x <= 0) return 0;\n    if (x >= 1) return 1;\n    let t = x;\n    for (let i = 0; i < 8; i++) {\n      const e = sx(t) - x;\n      if (Math.abs(e) < 1e-5) break;\n      const d = dx(t);\n      if (Math.abs(d) < 1e-6) break;\n      t = Math.min(1, Math.max(0, t - e / d));\n    }\n    return ((ay * t + by) * t + cy) * t;\n  };\n}\n\nconst IN_OUT = bezier(...TIMING.inOutCurve);\nconst EASE_OUT = bezier(...TIMING.easeOutCurve);\n\nconst mod = (n: number, m: number) => ((n % m) + m) % m;\n\n/** Where an option sits for a slot (0 is the socket), on a wheel of `count`. */\nfunction pose(slot: number, count: number) {\n  const a = (slot * WHEEL.step * Math.PI) / 180;\n  const d = Math.abs(slot);\n  const near = d <= 1 ? 1 - 0.2 * d : d <= 2 ? 0.8 - 0.3 * (d - 1) : Math.max(0, 0.5 * (1 - (d - 2) / (WHEEL.shown - 2)));\n  // On a short wheel the wrap point is close; fade out before it so nothing pops.\n  const wrap = Math.min(1, Math.max(0, (count / 2 - d) / 0.5));\n  return {\n    x: Math.sin(a) * WHEEL.radius, // share of the width\n    y: (1 - Math.cos(a)) * WHEEL.radius,\n    scale: 1 - WHEEL.shrink * Math.min(d, 3),\n    opacity: count === 1 ? 1 : near * wrap,\n  };\n}\n\n/** Wraps a slot into [-count/2, count/2). */\nconst wrapSlot = (s: number, count: number) => mod(s + count / 2, count) - count / 2;\n\nconst subscribeReduced = (onChange: () => void) => {\n  const query = window.matchMedia(REDUCED);\n  query.addEventListener(\"change\", onChange);\n  return () => query.removeEventListener(\"change\", onChange);\n};\n\n/** prefers-reduced-motion, false on the server and in the hydration render. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReduced,\n    () => window.matchMedia(REDUCED).matches,\n    () => false,\n  );\n}\n\nexport interface RerouteItem {\n  /** Stable key for the option. */\n  id: string;\n  /** Its name, shown under the socket while it's live and announced to screen readers. */\n  name: string;\n  /** Its mark: an inline SVG or an `<img>`, centered on the tile. */\n  icon: React.ReactNode;\n  /** A short line under the name while it's live, such as a latency or a region. */\n  detail?: string;\n}\n\n/** Why the live option changed: a pick (press, keys, drag), autoplay, an outage, or the picked option coming back. */\nexport type RerouteReason = \"pick\" | \"autoplay\" | \"failover\" | \"recover\";\n\nexport interface RerouteProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"defaultValue\" | \"onChange\"> {\n  /** The options round the wheel, in order. The one after the live option sits to its right. */\n  items: RerouteItem[];\n  /** What sits in the hub tile at the top: your mark, drawn in white on the accent. */\n  hub?: React.ReactNode;\n  /** The option you want routed to, when you control it. If it's down, the wheel sits on the next healthy one until it's back. */\n  value?: string;\n  /** The option routed to first, when you don't control it. Defaults to the first item. */\n  defaultValue?: string;\n  /** Called when the visitor picks an option, or autoplay moves on. */\n  onValueChange?: (id: string) => void;\n  /** Ids of options that are down. They dim and are skipped; if the live one goes down, the wheel reroutes to the next healthy option. */\n  down?: string[];\n  /** Called whenever the live option changes, with why. `null` when every option is down. */\n  onRoute?: (id: string | null, reason: RerouteReason) => void;\n  /** Turn to the next healthy option on a timer: `true` for every 2.8s, or a number of ms. Defaults to false. */\n  autoplay?: boolean | number;\n  /** Run requests down the cable as little comets. Defaults to true. */\n  flow?: boolean;\n  /** Ms each comet takes to run down the cable. Defaults to 1200. */\n  flowDuration?: number;\n  /** How fast a reroute plays, as a multiple of the default pace: the cable lifting and dropping back in, the wheel turning, the click, a bounce and the name changing. `2` is twice as fast. Defaults to 1. */\n  speed?: number;\n  /** Accessible name for the group of options. Defaults to \"Route\". */\n  label?: string;\n}\n\n/* ── Glyph ────────────────────────────────────────────────── */\n// The hub's default mark: a request splitting two ways.\n\nfunction HubGlyph() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n      <path d=\"M12 4v6\" />\n      <path d=\"M12 10c0 4-6 3-6 8M12 10c0 4 6 3 6 8\" />\n      <circle cx=\"12\" cy=\"4\" r=\"1.6\" fill=\"currentColor\" stroke=\"none\" />\n    </svg>\n  );\n}\n\n/* ── Reroute ──────────────────────────────────────────────── */\n\nexport function Reroute({\n  items,\n  hub,\n  value,\n  defaultValue,\n  onValueChange,\n  down,\n  onRoute,\n  autoplay = false,\n  flow = true,\n  flowDuration = TIMING.flowDuration,\n  speed = TIMING.speed,\n  label = \"Route\",\n  className,\n  onKeyDown,\n  ...props\n}: RerouteProps) {\n  const reduced = usePrefersReducedMotion();\n  const count = items.length;\n  const itemKey = items.map((i) => i.id).join(\"\\u0000\");\n  const downKey = (down ?? []).join(\"\\u0000\");\n  const downSet = React.useMemo(() => new Set(downKey ? downKey.split(\"\\u0000\") : []), [downKey]);\n\n  const [inner, setInner] = React.useState(() => defaultValue ?? items[0]?.id);\n  const preferred = value ?? inner;\n  const preferredIndex = Math.max(0, items.findIndex((i) => i.id === preferred));\n\n  // The live option: the preferred one, or the next healthy one round the wheel.\n  let liveIndex = -1;\n  for (let k = 0; k < count; k++) {\n    const i = (preferredIndex + k) % count;\n    if (!downSet.has(items[i].id)) {\n      liveIndex = i;\n      break;\n    }\n  }\n  const liveId = liveIndex >= 0 ? items[liveIndex].id : null;\n\n  // The first render places the wheel in CSS (cqw), so the server draws the rest layout.\n  const [initialLive] = React.useState(() => Math.max(0, liveIndex));\n  const [shown, setShown] = React.useState(() => Math.max(0, liveIndex));\n  const [message, setMessage] = React.useState(\"\");\n  const [inView, setInView] = React.useState(false);\n  const [held, setHeld] = React.useState(false); // pointer over it or focus inside: autoplay waits\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const wheelRef = React.useRef<HTMLDivElement>(null);\n  const cableRef = React.useRef<HTMLDivElement>(null);\n  const flowRef = React.useRef<HTMLDivElement>(null);\n  const bounceRef = React.useRef<HTMLSpanElement>(null);\n  const socketRef = React.useRef<HTMLSpanElement>(null);\n  const pulseRef = React.useRef<HTMLSpanElement>(null);\n  const failRef = React.useRef<HTMLSpanElement>(null);\n  const glowRef = React.useRef<HTMLSpanElement>(null);\n  const labelRef = React.useRef<HTMLDivElement>(null);\n  const tileEls = React.useRef(new Map<string, HTMLButtonElement>());\n  const engine = React.useRef<{\n    route: (index: number, reason: RerouteReason) => void;\n    sleep: (asleep: boolean) => void;\n    reflow: () => void;\n  } | null>(null);\n\n  const latest = React.useRef<{\n    items: RerouteItem[];\n    downSet: Set<string>;\n    liveIndex: number;\n    flow: boolean;\n    flowMs: number;\n    speed: number;\n    pick: (id: string, reason?: RerouteReason) => void;\n  }>({ items, downSet, liveIndex, flow, flowMs: flowDuration, speed, pick: () => {} });\n  const pendingReason = React.useRef<RerouteReason | null>(null);\n  const focusAfter = React.useRef<string | null>(null);\n\n  const pick = React.useCallback(\n    (id: string, reason: RerouteReason = \"pick\") => {\n      pendingReason.current = reason;\n      if (value === undefined) setInner(id);\n      onValueChange?.(id);\n    },\n    [value, onValueChange],\n  );\n\n  React.useLayoutEffect(() => {\n    latest.current = {\n      items,\n      downSet,\n      liveIndex,\n      flow,\n      flowMs: Math.max(100, flowDuration),\n      speed: Math.max(0.05, speed),\n      pick,\n    };\n  });\n\n  /* The engine: owns every transform on the wheel, the cable and the socket. */\n  React.useEffect(() => {\n    const root = rootRef.current;\n    const wheel = wheelRef.current;\n    const cable = cableRef.current;\n    const flowEl = flowRef.current;\n    const bounce = bounceRef.current;\n    const socket = socketRef.current;\n    const pulse = pulseRef.current;\n    const fail = failRef.current;\n    const glow = glowRef.current;\n    const labelEl = labelRef.current;\n    if (!root || !wheel || !cable || !flowEl || !bounce || !socket || !pulse || !fail || !glow || !labelEl) return;\n    const ids = itemKey ? itemKey.split(\"\\u0000\") : [];\n    const n = ids.length;\n    if (!n) return;\n\n    const start = Math.max(0, latest.current.liveIndex);\n    let width = root.clientWidth;\n    let measured = false;\n    let offset = start; // steps the wheel has turned; option k sits at slot k - offset\n    let goalOffset = start; // where the current turn ends\n    let turn: { from: number; to: number; start: number; duration: number; ease: (x: number) => number } | null = null;\n    let plugged = true;\n    let seated = true; // the cable has landed: comets may run\n    let asleep = true;\n    let target = start; // the option the wheel is heading for\n    let lastAnnounced = start;\n    const timers = new Set<number>();\n    const tileAnims = new Map<string, Animation>();\n    let flowAnims: Animation[] = [];\n    let glowAnim: Animation | null = null;\n    let cableAnim: Animation | null = null;\n\n    const later = (fn: () => void, ms: number) => {\n      const id = window.setTimeout(() => {\n        timers.delete(id);\n        fn();\n      }, ms);\n      timers.add(id);\n    };\n    /** A reroute's ms at the current `speed`. */\n    const paced = (ms: number) => ms / latest.current.speed;\n    const clearLater = () => {\n      timers.forEach((id) => window.clearTimeout(id));\n      timers.clear();\n    };\n\n    const transformFor = (p: ReturnType<typeof pose>) =>\n      `translate(${(p.x * width).toFixed(2)}px, ${(p.y * width).toFixed(2)}px) scale(${p.scale.toFixed(4)})`;\n\n    const now = () => performance.now();\n    const current = () => {\n      if (!turn) return offset;\n      const p = (now() - turn.start) / turn.duration;\n      if (p >= 1) {\n        // Finished, even if the timer that clears it was cancelled.\n        offset = turn.to;\n        turn = null;\n        return offset;\n      }\n      return turn.from + (turn.to - turn.from) * turn.ease(p);\n    };\n    /** A turn or a queued step is still under way. */\n    const busy = () => {\n      current(); // clears a turn that has run out\n      return timers.size > 0 || turn !== null;\n    };\n\n    /** Puts every option at its place for `at`, with no animation. */\n    const place = (at: number) => {\n      ids.forEach((id, k) => {\n        const el = tileEls.current.get(id);\n        if (!el) return;\n        tileAnims.get(id)?.cancel();\n        tileAnims.delete(id);\n        const s = wrapSlot(k - at, n);\n        const p = pose(s, n);\n        el.style.transform = transformFor(p);\n        el.style.opacity = String(p.opacity);\n        el.style.zIndex = String(10 - Math.round(Math.abs(s)));\n        el.style.pointerEvents = p.opacity < 0.05 ? \"none\" : \"\";\n      });\n    };\n\n    const freeze = () => {\n      const at = current();\n      turn = null;\n      offset = at;\n      place(at);\n    };\n\n    /** Glides the wheel from where it is to `to`, round the rim. */\n    const turnTo = (to: number, duration: number, easing: string, ease: (x: number) => number) => {\n      const from = current();\n      freeze();\n      const delta = to - from;\n      if (Math.abs(delta) < 1e-4) {\n        offset = to;\n        place(to);\n        return;\n      }\n      turn = { from, to, start: now(), duration, ease };\n      const samples = Math.max(2, Math.ceil(Math.abs(delta) * 10));\n      ids.forEach((id, k) => {\n        const el = tileEls.current.get(id);\n        if (!el) return;\n        const frames: Keyframe[] = [];\n        let prev: number | null = null;\n        let seen = false;\n        for (let j = 0; j <= samples; j++) {\n          const u = j / samples;\n          const raw = k - (from + delta * u);\n          const s = wrapSlot(raw, n);\n          // Crossing the far side of the wheel: two frames at the same offset, both out of sight.\n          if (prev !== null && Math.abs(s - prev) > n / 2) {\n            const edge = prev > 0 ? n / 2 - 1e-3 : -n / 2;\n            const back = prev > 0 ? -n / 2 : n / 2 - 1e-3;\n            const pu = (j - 0.5) / samples;\n            frames.push({ offset: pu, transform: transformFor(pose(edge, n)), opacity: 0 });\n            frames.push({ offset: pu, transform: transformFor(pose(back, n)), opacity: 0 });\n          }\n          const p = pose(s, n);\n          if (p.opacity > 0.01) seen = true;\n          frames.push({ offset: u, transform: transformFor(p), opacity: p.opacity });\n          prev = s;\n        }\n        const end = pose(wrapSlot(k - to, n), n);\n        el.style.transform = transformFor(end);\n        el.style.opacity = String(end.opacity);\n        el.style.zIndex = String(10 - Math.round(Math.abs(wrapSlot(k - to, n))));\n        el.style.pointerEvents = end.opacity < 0.05 ? \"none\" : \"\";\n        if (!seen) return; // out of sight the whole way: just move it\n        const anim = el.animate(frames, { duration, easing });\n        tileAnims.set(id, anim);\n        anim.onfinish = () => {\n          if (tileAnims.get(id) === anim) {\n            anim.cancel();\n            tileAnims.delete(id);\n          }\n        };\n      });\n      offset = to;\n      later(() => {\n        if (turn && turn.to === to) turn = null;\n      }, duration);\n    };\n\n    /* Cable, comets, socket */\n\n    /** How far the cable is pulled up right now, in % of its length (0 is plugged in). */\n    const cableLift = () => {\n      const m = getComputedStyle(cable).transform;\n      const v = m && m !== \"none\" ? m.match(/matrix\\(([^)]+)\\)/) : null;\n      const ty = v ? Number(v[1].split(\",\")[5]) : 0;\n      return cable.clientHeight ? (ty / cable.clientHeight) * 100 : 0;\n    };\n\n    const animateCable = (frames: number[], offsets: number[] | null, duration: number, easing: string) => {\n      const from = cableLift();\n      cableAnim?.cancel();\n      const kf: Keyframe[] = [\n        { transform: `translateY(${from}%)` },\n        ...frames.map((f, i) => ({ transform: `translateY(${f}%)`, offset: offsets?.[i] })),\n      ];\n      kf[kf.length - 1].offset = 1;\n      cable.style.transform = `translateY(${frames[frames.length - 1]}%)`;\n      cableAnim = cable.animate(kf, { duration, easing });\n    };\n\n    let flowMs = latest.current.flowMs;\n    const startFlow = () => {\n      flowAnims.forEach((a) => a.cancel());\n      flowAnims = [];\n      flowMs = latest.current.flowMs;\n      const length = cable.clientHeight;\n      Array.from(flowEl.children).forEach((child, i) => {\n        const anim = (child as HTMLElement).animate(\n          [\n            { transform: \"translateY(-12px)\", opacity: 0 },\n            { opacity: 1, offset: 0.18 },\n            { opacity: 1, offset: 0.8 },\n            { transform: `translateY(${length - 12}px)`, opacity: 0 },\n          ],\n          { duration: flowMs, iterations: Infinity, delay: -(i * flowMs) / WHEEL.flow.count },\n        );\n        flowAnims.push(anim);\n      });\n      syncFlow();\n    };\n\n    const syncFlow = () => {\n      const on = plugged && seated && !asleep && latest.current.flow && !reduced && latest.current.liveIndex >= 0;\n      flowAnims.forEach((a) => (on ? a.play() : a.pause()));\n      flowEl.style.opacity = on ? \"1\" : \"0\";\n      if (glowAnim) {\n        if (asleep || reduced) glowAnim.pause();\n        else glowAnim.play();\n      }\n    };\n\n    const unplug = () => {\n      if (!plugged) return;\n      plugged = false;\n      seated = false;\n      syncFlow();\n      if (reduced) return;\n      animateCable([-WHEEL.lift.by], null, paced(TIMING.lift), TIMING.easeOut);\n      socket.animate([{ opacity: 1 }, { opacity: 0.25 }], { duration: paced(TIMING.lift), easing: TIMING.fade, fill: \"forwards\" });\n    };\n\n    /** The cable lands: the socket clicks and the comets come back. */\n    const seat = () => {\n      seated = true;\n      socket.getAnimations().forEach((a) => a.cancel());\n      socket.animate(\n        [\n          { transform: `scale(${WHEEL.click.from})`, opacity: 0.4 },\n          { transform: \"scale(1)\", opacity: 1 },\n        ],\n        { duration: paced(TIMING.click), easing: TIMING.easeOut },\n      );\n      pulse.animate(\n        [\n          { transform: \"scale(1)\", opacity: 0.5 },\n          { transform: `scale(${WHEEL.click.pulse})`, opacity: 0 },\n        ],\n        { duration: paced(TIMING.pulse), easing: TIMING.easeOut },\n      );\n      syncFlow();\n    };\n\n    /** Lowers the cable back into the socket, from wherever it is. */\n    const plug = () => {\n      if (plugged) return;\n      plugged = true;\n      if (reduced) {\n        seated = true;\n        syncFlow();\n        return;\n      }\n      const drop = paced(TIMING.drop.duration);\n      animateCable([0], null, drop, TIMING.lower);\n      // The in-out curve has covered ~98% of the way by 82%: seat it there, as it touches down.\n      later(seat, Math.round(drop * TIMING.drop.seat));\n    };\n\n    const labelOut = () => {\n      labelEl.getAnimations().forEach((a) => a.cancel());\n      labelEl.style.opacity = \"0\";\n      labelEl.animate([{ opacity: 1 }, { opacity: 0 }], { duration: paced(TIMING.label.out), easing: TIMING.fade });\n    };\n    const labelIn = (index: number, reason: RerouteReason | null) => {\n      setShown(index);\n      const item = latest.current.items[index];\n      if (item && reason) {\n        const prev = latest.current.items[mod(Math.round(lastAnnounced), n)];\n        setMessage(\n          reason === \"failover\" && prev && prev.id !== item.id\n            ? `${prev.name} is down. Rerouted to ${item.name}.`\n            : reason === \"recover\"\n              ? `${item.name} is back. Routing to ${item.name}.`\n              : `Routing to ${item.name}.`,\n        );\n      }\n      lastAnnounced = index;\n      labelEl.getAnimations().forEach((a) => a.cancel());\n      labelEl.style.opacity = \"1\";\n      labelEl.animate(\n        reduced\n          ? [{ opacity: 0 }, { opacity: 1 }]\n          : [\n              { opacity: 0, transform: `translateY(${WHEEL.label.rise}px)` },\n              { opacity: 1, transform: \"translateY(0)\" },\n            ],\n        { duration: reduced ? TIMING.crossFade : paced(TIMING.label.in), easing: TIMING.easeOut },\n      );\n    };\n    const fault = () => {\n      if (reduced) return;\n      const length = cable.clientHeight;\n      bounce.animate(\n        [\n          { transform: `translateY(${length - 14}px)`, opacity: 1 },\n          { transform: `translateY(${length * 0.4}px)`, opacity: 0 },\n        ],\n        { duration: paced(TIMING.bounce.duration), easing: TIMING.easeOut },\n      );\n      fail.animate(\n        [\n          { transform: \"scale(1)\", opacity: 0.9 },\n          { transform: \"scale(1.08)\", opacity: 0 },\n        ],\n        { duration: paced(TIMING.fail), easing: TIMING.easeOut },\n      );\n    };\n\n    /** Takes the wheel to option `index`, for `reason`. */\n    const route = (index: number, reason: RerouteReason, settle?: number) => {\n      // Already on its way there (a pick echoing back from the parent): leave it be.\n      if (index === target && settle === undefined && (reason === \"pick\" || reason === \"autoplay\") && busy()) return;\n      clearLater();\n      if (index < 0) {\n        // Nothing healthy: lift the cable, dim the name and wait.\n        if (reason === \"failover\") fault();\n        later(unplug, reason === \"failover\" ? paced(TIMING.bounce.lead) : 0);\n        labelEl.getAnimations().forEach((a) => a.cancel());\n        labelEl.animate([{ opacity: labelEl.style.opacity || \"1\" }, { opacity: 0.4 }], { duration: paced(TIMING.dim), easing: TIMING.fade });\n        labelEl.style.opacity = \"0.4\";\n        setMessage(\"Every option is down.\");\n        return;\n      }\n      const at = current();\n      let goal: number;\n      if (settle !== undefined) goal = settle;\n      else {\n        const forward = mod(index - at, n);\n        const back = forward - n;\n        const forwardOnly = reason === \"failover\" || reason === \"autoplay\";\n        goal = Math.round(at + (forwardOnly || Math.abs(forward) <= Math.abs(back) ? forward : back));\n      }\n      target = index;\n      goalOffset = goal;\n      if (Math.abs(goal - at) < 1e-3 && plugged) {\n        offset = goal;\n        return;\n      }\n      if (reduced) {\n        turn = null;\n        offset = goal;\n        place(goal);\n        plugged = true;\n        seated = true;\n        syncFlow();\n        labelIn(index, reason);\n        return;\n      }\n      const lead = reason === \"failover\" ? paced(TIMING.bounce.lead) : 0;\n      if (reason === \"failover\") fault();\n      const steps = Math.max(1, Math.ceil(Math.abs(goal - at) - 1e-3));\n      const duration = paced(\n        settle !== undefined ? TIMING.settle : Math.min(TIMING.turn.max, TIMING.turn.base + TIMING.turn.per * (steps - 1)),\n      );\n      const go = () => {\n        unplug();\n        labelOut();\n        later(\n          () => {\n            if (settle !== undefined) turnTo(goal, duration, TIMING.easeOut, EASE_OUT);\n            else turnTo(goal, duration, TIMING.inOut, IN_OUT);\n            later(plug, Math.max(0, duration - paced(TIMING.drop.lead)));\n            later(() => labelIn(index, reason), Math.max(0, duration - paced(TIMING.labelLead)));\n          },\n          plugged ? paced(TIMING.turnLag) : 0,\n        );\n      };\n      if (lead) later(go, lead);\n      else go();\n    };\n\n    /* Drag to spin */\n\n    let drag: { id: number; x: number; from: number; moved: boolean; samples: { t: number; at: number }[] } | null = null;\n    let raf = 0;\n    let dragAt = 0;\n    let suppressClick = false;\n\n    const stepPx = () => width * WHEEL.radius * ((WHEEL.step * Math.PI) / 180);\n\n    const onPointerDown = (e: PointerEvent) => {\n      if (e.button !== 0 || n < 2) return;\n      drag = { id: e.pointerId, x: e.clientX, from: current(), moved: false, samples: [] };\n    };\n    const onPointerMove = (e: PointerEvent) => {\n      if (!drag || e.pointerId !== drag.id) return;\n      const dx = e.clientX - drag.x;\n      if (!drag.moved) {\n        if (Math.abs(dx) < WHEEL.drag.slop) return;\n        drag.moved = true;\n        clearLater();\n        freeze();\n        drag.from = offset;\n        drag.x = e.clientX;\n        unplug();\n        labelOut();\n        wheel.setPointerCapture(e.pointerId);\n        return;\n      }\n      dragAt = drag.from - dx / stepPx();\n      const t = now();\n      drag.samples.push({ t, at: dragAt });\n      while (drag.samples.length > 2 && t - drag.samples[0].t > TIMING.drag.window) drag.samples.shift();\n      if (!raf)\n        raf = requestAnimationFrame(() => {\n          raf = 0;\n          offset = dragAt;\n          place(dragAt);\n        });\n    };\n    const onPointerUp = (e: PointerEvent) => {\n      if (!drag || e.pointerId !== drag.id) return;\n      const d = drag;\n      drag = null;\n      if (!d.moved) return;\n      suppressClick = true;\n      later(() => (suppressClick = false), 0);\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n      offset = dragAt;\n      place(dragAt);\n      const first = d.samples[0];\n      const last = d.samples[d.samples.length - 1];\n      const speed = first && last && last.t > first.t ? (last.at - first.at) / (last.t - first.t) : 0;\n      const carry = reduced ? 0 : speed * TIMING.drag.carry;\n      let goal = Math.round(dragAt + carry);\n      const dir = Math.sign(carry || goal - d.from || 1);\n      // Skip options that are down, carrying on the way it was spun.\n      const { items: list, downSet: off } = latest.current;\n      for (let k = 0; k < n && off.has(list[mod(goal, n)]?.id); k++) goal += dir;\n      if (off.has(list[mod(goal, n)]?.id)) {\n        route(latest.current.liveIndex, \"pick\");\n        return;\n      }\n      const index = mod(goal, n);\n      route(index, \"pick\", goal);\n      if (index !== latest.current.liveIndex) latest.current.pick(list[index].id);\n      // A controlled wheel that didn't take the pick goes back to the live option.\n      later(() => {\n        if (latest.current.liveIndex !== target) route(latest.current.liveIndex, \"pick\");\n      }, TIMING.revert);\n    };\n    const onClickCapture = (e: MouseEvent) => {\n      if (suppressClick) {\n        e.stopPropagation();\n        e.preventDefault();\n      }\n    };\n\n    wheel.addEventListener(\"pointerdown\", onPointerDown);\n    wheel.addEventListener(\"pointermove\", onPointerMove);\n    wheel.addEventListener(\"pointerup\", onPointerUp);\n    wheel.addEventListener(\"pointercancel\", onPointerUp);\n    wheel.addEventListener(\"click\", onClickCapture, true);\n\n    /* Size, sleep */\n\n    // A new width snaps everything to where it was heading: the turn's keyframes are in px.\n    const ro = new ResizeObserver(() => {\n      const w = root.clientWidth;\n      if (!w || (measured && Math.abs(w - width) < 0.5)) return;\n      const settled = measured && (turn !== null || !plugged);\n      measured = true;\n      width = w;\n      if (settled) {\n        clearLater();\n        turn = null;\n        offset = goalOffset;\n        cableAnim?.cancel();\n        cable.style.transform = \"\";\n        socket.getAnimations().forEach((a) => a.cancel());\n        plugged = latest.current.liveIndex >= 0;\n        seated = plugged;\n        setShown(mod(Math.round(goalOffset), n));\n        labelEl.getAnimations().forEach((a) => a.cancel());\n        labelEl.style.opacity = plugged ? \"1\" : \"0.4\";\n      }\n      place(offset);\n      startFlow();\n    });\n    ro.observe(root);\n\n    if (!reduced) {\n      glowAnim = glow.animate([{ opacity: 0.5 }, { opacity: 1 }], {\n        duration: TIMING.glow.duration,\n        iterations: Infinity,\n        direction: \"alternate\",\n        easing: TIMING.glow.easing,\n      });\n    } else glow.style.opacity = \"0.8\";\n\n    place(offset);\n    startFlow();\n\n    engine.current = {\n      route,\n      sleep: (value) => {\n        asleep = value;\n        syncFlow();\n      },\n      reflow: () => {\n        if (latest.current.flowMs !== flowMs) startFlow();\n      },\n    };\n\n    return () => {\n      engine.current = null;\n      clearLater();\n      ro.disconnect();\n      if (raf) cancelAnimationFrame(raf);\n      tileAnims.forEach((a) => a.cancel());\n      flowAnims.forEach((a) => a.cancel());\n      glowAnim?.cancel();\n      cableAnim?.cancel();\n      wheel.removeEventListener(\"pointerdown\", onPointerDown);\n      wheel.removeEventListener(\"pointermove\", onPointerMove);\n      wheel.removeEventListener(\"pointerup\", onPointerUp);\n      wheel.removeEventListener(\"pointercancel\", onPointerUp);\n      wheel.removeEventListener(\"click\", onClickCapture, true);\n    };\n  }, [itemKey, reduced]);\n\n  // A new `flowDuration` restarts the comets at the new pace.\n  React.useEffect(() => {\n    engine.current?.reflow();\n  }, [flowDuration]);\n\n  /* Route on every change of the live option, and say why. */\n  const prev = React.useRef<{ live: number; preferred: number } | null>(null);\n  React.useLayoutEffect(() => {\n    const before = prev.current;\n    prev.current = { live: liveIndex, preferred: preferredIndex };\n    if (!before) return;\n    if (before.live === liveIndex) {\n      pendingReason.current = null;\n      return;\n    }\n    let reason: RerouteReason;\n    if (pendingReason.current) reason = pendingReason.current;\n    else if (before.live < 0 && liveIndex >= 0) reason = \"recover\";\n    else if (before.live >= 0 && downSet.has(items[before.live]?.id)) reason = \"failover\";\n    else if (liveIndex === preferredIndex && before.live !== before.preferred) reason = \"recover\";\n    else reason = liveIndex < 0 ? \"failover\" : \"pick\";\n    pendingReason.current = null;\n    engine.current?.route(liveIndex, reason);\n    onRoute?.(liveId, reason);\n    if (focusAfter.current) {\n      const el = tileEls.current.get(focusAfter.current);\n      focusAfter.current = null;\n      el?.focus({ preventScroll: true });\n    }\n  }, [liveIndex, preferredIndex, downSet, items, liveId, onRoute]);\n\n  /* Sleep off screen and on a hidden tab. */\n  React.useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    let visible = false;\n    const sync = () => {\n      const awake = visible && document.visibilityState === \"visible\";\n      engine.current?.sleep(!awake);\n      setInView(awake);\n    };\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      sync();\n    });\n    io.observe(root);\n    document.addEventListener(\"visibilitychange\", sync);\n    return () => {\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", sync);\n    };\n  }, [itemKey, reduced]);\n\n  /* Autoplay */\n  const every = autoplay === true ? TIMING.autoplay : autoplay === false ? 0 : autoplay;\n  React.useEffect(() => {\n    if (!every || reduced || !inView || held || liveIndex < 0 || count < 2) return;\n    const id = window.setTimeout(() => {\n      const { items: list, downSet: off, liveIndex: live, pick: go } = latest.current;\n      for (let k = 1; k < list.length; k++) {\n        const next = list[(live + k) % list.length];\n        if (!off.has(next.id)) {\n          go(next.id, \"autoplay\");\n          return;\n        }\n      }\n    }, every);\n    return () => window.clearTimeout(id);\n  }, [every, reduced, inView, held, liveIndex, count, downKey]);\n\n  const step = (dir: 1 | -1) => {\n    if (liveIndex < 0) return;\n    for (let k = 1; k < count; k++) {\n      const next = items[mod(liveIndex + dir * k, count)];\n      if (!downSet.has(next.id)) {\n        focusAfter.current = next.id;\n        pick(next.id);\n        return;\n      }\n    }\n  };\n\n  const shownItem = items[shown] ?? items[0];\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"@container/reroute relative w-full select-none\",\n        \"[--reroute-accent:#ff3b1f] [--reroute-down:#18181b] dark:[--reroute-accent:#ff5a3c] dark:[--reroute-down:#e4e4e7]\",\n        className,\n      )}\n      onPointerEnter={(e) => e.pointerType === \"mouse\" && setHeld(true)}\n      onPointerLeave={(e) => e.pointerType === \"mouse\" && setHeld(false)}\n      onFocus={() => setHeld(true)}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node)) setHeld(false);\n      }}\n      onKeyDown={(e) => {\n        onKeyDown?.(e);\n        if (e.defaultPrevented) return;\n        if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n          e.preventDefault();\n          step(1);\n        } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n          e.preventDefault();\n          step(-1);\n        }\n      }}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"relative h-[var(--reroute-height,clamp(15rem,54cqw,21rem))] w-full overflow-x-clip\",\n          \"[--t:clamp(3rem,11cqw,4rem)] [--l:clamp(2.5rem,11cqw,4.5rem)]\",\n          \"[--apex:calc(0.5rem+var(--t)+var(--l)+5px+var(--t)/2)]\",\n        )}\n      >\n        {/* The hub, its glow and the cable */}\n        <span\n          ref={glowRef}\n          aria-hidden\n          className=\"pointer-events-none absolute left-1/2 top-[calc(0.5rem+var(--t)/2)] size-[calc(var(--t)*3.2)] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--reroute-accent)_34%,transparent),transparent)] will-change-[opacity]\"\n        />\n        <div\n          aria-hidden\n          className=\"absolute left-1/2 top-2 z-20 grid size-[var(--t)] -translate-x-1/2 place-items-center rounded-[calc(var(--t)*0.3)] bg-[linear-gradient(180deg,color-mix(in_oklab,var(--reroute-accent),white_16%),var(--reroute-accent))] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.35),0_8px_20px_-6px_color-mix(in_oklab,var(--reroute-accent)_60%,transparent)] [&_svg]:size-[46%]\"\n        >\n          {hub ?? <HubGlyph />}\n        </div>\n        <div\n          ref={cableRef}\n          aria-hidden\n          className=\"absolute left-[calc(50%-0.5px)] top-[calc(0.5rem+var(--t))] z-10 h-[var(--l)] w-px bg-[color-mix(in_oklab,var(--reroute-accent)_45%,transparent)] will-change-transform\"\n        >\n          <span className=\"absolute -bottom-px left-1/2 h-1.5 w-[5px] -translate-x-1/2 rounded-[2px] bg-[var(--reroute-accent)]\" />\n          <div ref={flowRef} className={`absolute inset-x-0 top-0 h-0 transition-opacity ${TIMING.flowFade}`}>\n            {Array.from({ length: WHEEL.flow.count }, (_, i) => (\n              <span\n                key={i}\n                className=\"absolute left-1/2 top-0 h-3 w-[3px] -translate-x-1/2 rounded-full bg-[linear-gradient(to_bottom,transparent,var(--reroute-accent))] opacity-0 will-change-transform\"\n              />\n            ))}\n          </div>\n          <span\n            ref={bounceRef}\n            className=\"absolute left-1/2 top-0 h-3 w-[3px] -translate-x-1/2 rounded-full bg-[linear-gradient(to_top,transparent,var(--reroute-down))] opacity-0 will-change-transform\"\n          />\n        </div>\n\n        {/* The wheel */}\n        <div\n          ref={wheelRef}\n          className=\"absolute inset-0 touch-pan-y\"\n          style={{\n            maskImage:\n              \"linear-gradient(to right, transparent, #000 13%, #000 87%, transparent), linear-gradient(to bottom, #000 72%, transparent)\",\n            WebkitMaskImage:\n              \"linear-gradient(to right, transparent, #000 13%, #000 87%, transparent), linear-gradient(to bottom, #000 72%, transparent)\",\n            maskComposite: \"intersect\",\n            WebkitMaskComposite: \"source-in\",\n          }}\n        >\n          <span\n            aria-hidden\n            className=\"pointer-events-none absolute left-[calc(50%-72cqw)] top-[var(--apex)] size-[144cqw] rounded-full border border-zinc-950/[0.08] dark:border-white/10\"\n          />\n          <span\n            aria-hidden\n            className=\"pointer-events-none absolute left-[calc(50%-72cqw+var(--t)*0.9)] top-[calc(var(--apex)+var(--t)*0.9)] size-[calc(144cqw-var(--t)*1.8)] rounded-full border border-dashed border-zinc-950/[0.06] dark:border-white/[0.07]\"\n          />\n          <div role=\"radiogroup\" aria-label={label} className=\"absolute inset-0\">\n            {items.map((item, k) => {\n              const s = wrapSlot(k - initialLive, count);\n              const p = pose(s, count);\n              const isDown = downSet.has(item.id);\n              const isLive = k === liveIndex;\n              return (\n                <button\n                  key={item.id}\n                  ref={(el) => {\n                    if (el) tileEls.current.set(item.id, el);\n                    else tileEls.current.delete(item.id);\n                  }}\n                  type=\"button\"\n                  role=\"radio\"\n                  aria-checked={isLive}\n                  aria-disabled={isDown || undefined}\n                  aria-label={isDown ? `${item.name} (down)` : item.name}\n                  tabIndex={isLive || (liveIndex < 0 && k === 0) ? 0 : -1}\n                  data-down={isDown || undefined}\n                  data-live={isLive || undefined}\n                  onClick={() => {\n                    if (isDown || isLive) return;\n                    pick(item.id);\n                  }}\n                  className={cn(\n                    \"group/tile absolute left-[calc(50%-var(--t)/2)] top-[calc(var(--apex)-var(--t)/2)] grid size-[var(--t)] place-items-center rounded-[calc(var(--t)*0.28)] will-change-[transform,opacity]\",\n                    \"border border-zinc-950/[0.08] bg-white text-zinc-950 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_6px_16px_-8px_rgba(0,0,0,0.12)] dark:border-white/10 dark:bg-zinc-900 dark:text-white\",\n                    \"outline-offset-4 outline-[var(--reroute-accent)] focus-visible:outline-2\",\n                    `transition-[border-color] ${TIMING.hover} [@media(hover:hover)]:hover:border-zinc-950/20 dark:[@media(hover:hover)]:hover:border-white/25`,\n                    isDown ? \"cursor-not-allowed\" : isLive ? \"cursor-default\" : \"cursor-pointer\",\n                  )}\n                  style={{\n                    transform: `translate(calc(${p.x.toFixed(4)} * 100cqw), calc(${p.y.toFixed(4)} * 100cqw)) scale(${p.scale.toFixed(4)})`,\n                    opacity: p.opacity,\n                    zIndex: 10 - Math.round(Math.abs(s)),\n                  }}\n                >\n                  <span\n                    className={cn(\n                      `grid size-[46%] place-items-center transition-opacity ${TIMING.down} [&>*]:max-h-full [&>*]:max-w-full`,\n                      isDown && \"opacity-30 grayscale\",\n                    )}\n                  >\n                    {item.icon}\n                  </span>\n                  <span\n                    aria-hidden\n                    className={cn(\n                      `absolute -right-1 -top-1 size-2.5 rounded-full bg-[var(--reroute-down)] ring-2 ring-white transition-[opacity,scale] ${TIMING.down} dark:ring-zinc-900`,\n                      isDown ? \"scale-100 opacity-100\" : \"scale-50 opacity-0\",\n                    )}\n                  />\n                </button>\n              );\n            })}\n          </div>\n        </div>\n\n        {/* The socket: fixed at the top of the wheel, the options turn through it */}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute left-1/2 top-[var(--apex)] z-30 size-[calc(var(--t)+10px)] -translate-x-1/2 -translate-y-1/2\"\n        >\n          <span\n            ref={socketRef}\n            className=\"absolute inset-0 rounded-[calc(var(--t)*0.28+5px)] border-[1.5px] border-[var(--reroute-accent)] shadow-[0_0_0_4px_color-mix(in_oklab,var(--reroute-accent)_10%,transparent),0_10px_28px_-8px_color-mix(in_oklab,var(--reroute-accent)_45%,transparent)] will-change-[transform,opacity]\"\n          />\n          <span\n            ref={pulseRef}\n            className=\"absolute inset-0 rounded-[calc(var(--t)*0.28+5px)] border border-[var(--reroute-accent)] opacity-0 will-change-[transform,opacity]\"\n          />\n          <span\n            ref={failRef}\n            className=\"absolute -inset-[3px] rounded-[calc(var(--t)*0.28+8px)] border-2 border-[var(--reroute-down)] opacity-0 will-change-[transform,opacity]\"\n          />\n        </span>\n\n        {/* What's live */}\n        <div\n          ref={labelRef}\n          aria-hidden\n          className=\"pointer-events-none absolute left-1/2 top-[calc(var(--apex)+var(--t)/2+16px)] z-30 w-[min(calc(86cqw-var(--t)*0.72-1rem),17rem)] -translate-x-1/2 text-center will-change-[transform,opacity]\"\n        >\n          {liveIndex >= 0 || shownItem ? (\n            <>\n              <div className=\"truncate text-[13px] font-medium leading-5 tracking-tight text-zinc-950 dark:text-white\">\n                {shownItem?.name}\n              </div>\n              {shownItem?.detail ? (\n                <div className=\"truncate font-mono text-[11px] leading-4 tabular-nums text-zinc-500 dark:text-zinc-400\">\n                  {shownItem.detail}\n                </div>\n              ) : null}\n            </>\n          ) : null}\n        </div>\n      </div>\n      <p className=\"sr-only\" aria-live=\"polite\">\n        {message}\n      </p>\n    </div>\n  );\n}\n"
    }
  ]
}