{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "circuit",
  "type": "registry:ui",
  "title": "Circuit",
  "description": "Right-angle circuit traces between the cards you already have, with signals that travel through them. Mark any element with `data-circuit`, describe the graph in one string, and the traces route themselves: edge to edge side by side, top to bottom when the layout stacks, around anything in the way.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "diagram",
    "svg",
    "interaction",
    "landing"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/circuit",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/circuit.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - Nodes are your own elements marked `data-circuit`, with no wrappers or refs; every trace is\n *   drawn in one SVG under the cards (`-z-10` inside the circuit's own stacking context).\n * - One rAF loop measures every node box each frame while on screen and reroutes only when one\n *   moved; it sleeps when idle or off screen, and pauses time instead of replaying it on return.\n * - Draw-in and packets are SVG attributes (dash arrays, positions) written from that loop, never\n *   React state.\n * - A packet's head eases to the far pin, then its tail drains into the card at full speed; a card\n *   passes on only the first of several signals within `cooldown`.\n * - Under reduced motion nothing draws in or travels: a signal lands everywhere it would have gone,\n *   at once.\n */\nconst TIMING = {\n  /** Between signals from each source, in seconds (the `interval` prop). */\n  interval: 2.4,\n  /** A packet's speed along its trace, in px/s (the `speed` prop). */\n  speed: 260,\n  /** The shortest and longest a packet's trip takes, whatever its trace's length, in seconds. */\n  trip: { min: 0.45, max: 2.4 },\n  /** A card holding a signal before it passes it on, in seconds. */\n  relay: 0.1,\n  /** Seconds within which a card passes on only the first of several signals. */\n  cooldown: 0.3,\n  /** Seconds a card keeps data-circuit-pulse after a signal lands; the `pulseDuration` prop. */\n  pulseDuration: 0.5,\n  /** The landing flash on the pin, in seconds. */\n  flash: 0.6,\n  /** Seconds a trace takes to draw in; the `drawDuration` prop. */\n  drawDuration: 0.7,\n  /** Seconds between stages drawing in on mount; the `drawStagger` prop. */\n  drawStagger: 0.16,\n  /** Hovering a card sends a signal unless it sent one this recently, in seconds. */\n  resend: 0.6,\n  /** After the pointer leaves a card, before the highlight clears, in ms. */\n  grace: 90,\n  /** Traces dimming and lighting for a highlight, as a Tailwind class. */\n  highlight: \"duration-300\",\n  /** The loop running on after a change (a resize, a transition starting), in seconds. */\n  awake: 1,\n  /** And past a pulse's `pulseDuration`, so the attribute clears on time, in seconds. */\n  pastPulse: 0.1,\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * CIRCUIT STORYBOARD\n *\n *  nodes      any element inside <Circuit> with data-circuit=\"id\".\n *             No wrappers, no refs: your grid or flex layout stays\n *             exactly as it is\n *  flow       one string draws the graph:\n *             \"github, slack -> agent -> pr, docs\"\n *  measure    every frame while it's on screen, the node boxes are\n *             read relative to the circuit, so lines follow resizes,\n *             font swaps, hover lifts and layout animations\n *  route      each trace tries a few right-angle shapes (Z, L, or a\n *             U around the side) and keeps the one that crosses no\n *             other card with the fewest bends. Side-by-side cards\n *             connect edge to edge; stacked on a phone they connect\n *             top to bottom or detour down a gutter\n *  pins       traces sharing a card edge spread into separate pins,\n *             sorted so they never cross; traces sharing a channel\n *             spread apart only where they would overlap\n *  draw       on mount the traces draw themselves in, stage by stage\n *  signal     a packet leaves each source, eases along its trace and\n *             lands: the card gets data-circuit-pulse, the pin\n *             flashes, onArrive fires, and the card passes it on\n *  hover      pointing at (or focusing, or tapping) a card lights\n *             the path through it, dims the rest and sends a signal\n * ───────────────────────────────────────────────────────── */\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport type CircuitTrace = {\n  from: string;\n  to: string;\n  /** Packet color for this trace (any CSS color). */\n  color?: string;\n  /** Pin the side the trace leaves `from` by. Picked automatically otherwise. */\n  fromSide?: Side;\n  /** Pin the side the trace enters `to` by. Picked automatically otherwise. */\n  toSide?: Side;\n};\n\n/**\n * `\"a, b -> c -> d, e\"`: every node in a stage connects to every node in\n * the next. Separate chains with `;` or a new line. Mix in trace objects\n * for per-trace options.\n */\nexport type CircuitFlow = string | ReadonlyArray<string | CircuitTrace>;\n\nexport type CircuitControls = {\n  /** Send a signal out of a node, as if one had just arrived there. */\n  send: (node: string) => void;\n};\n\nexport interface CircuitProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Which nodes connect. */\n  flow: CircuitFlow;\n  /** Sources (nodes nothing flows into) send signals on their own. */\n  autoPlay?: boolean;\n  /** Seconds between signals from each source. */\n  interval?: number;\n  /** Packet speed in px per second. */\n  speed?: number;\n  /** A node with several outgoing traces sends a signal down all of them, or down one at a time in turn. */\n  fanout?: \"all\" | \"one\";\n  /** What lights up when a node is hovered, focused or tapped. */\n  highlight?: \"path\" | \"neighbors\" | \"none\";\n  /** Corner radius of the bends, in px. */\n  radius?: number;\n  /** Space between pins sharing a card edge, and between traces sharing a channel, in px. */\n  pinSpacing?: number;\n  /** Draw a pin where a trace meets a card. */\n  pins?: boolean;\n  /** Length of a packet's glowing tail, in px. */\n  tailLength?: number;\n  /** Seconds a card keeps `data-circuit-pulse` after a signal lands. */\n  pulseDuration?: number;\n  /** Seconds each trace takes to draw itself in on mount. */\n  drawDuration?: number;\n  /** Seconds between the stages drawing in on mount, source side first. */\n  drawStagger?: number;\n  /** Called when a signal lands on a node. */\n  onArrive?: (node: string, from: string) => void;\n  /** `send(node)` fires a signal from code, e.g. a button or a real event. */\n  apiRef?: React.Ref<CircuitControls>;\n  /** Text color classes for the traces and pins. */\n  traceClassName?: string;\n  /** Text color classes for the packets and lit traces. */\n  packetClassName?: string;\n}\n\nexport function Circuit({\n  flow,\n  autoPlay = true,\n  interval = TIMING.interval,\n  speed = TIMING.speed,\n  fanout = \"all\",\n  highlight = \"path\",\n  radius = 12,\n  pinSpacing = 12,\n  pins = true,\n  tailLength = TAIL,\n  pulseDuration = TIMING.pulseDuration,\n  drawDuration = TIMING.drawDuration,\n  drawStagger = TIMING.drawStagger,\n  onArrive,\n  apiRef,\n  traceClassName,\n  packetClassName,\n  className,\n  children,\n  ...props\n}: CircuitProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const svgRef = React.useRef<SVGSVGElement>(null);\n  const engineRef = React.useRef<Engine | null>(null);\n  const onArriveRef = React.useRef(onArrive);\n\n  React.useEffect(() => {\n    onArriveRef.current = onArrive;\n  }, [onArrive]);\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    const svg = svgRef.current;\n    if (!root || !svg) return;\n    const engine = createEngine(root, svg, (node, from) => onArriveRef.current?.(node, from));\n    engineRef.current = engine;\n    return () => {\n      engine.destroy();\n      engineRef.current = null;\n    };\n  }, []);\n\n  const trace = cn(\"text-zinc-300 dark:text-zinc-700\", traceClassName);\n  const packet = cn(\"text-sky-500 dark:text-sky-400\", packetClassName);\n\n  React.useEffect(() => {\n    engineRef.current?.configure({\n      flow,\n      autoPlay,\n      interval,\n      speed,\n      fanout,\n      highlight,\n      radius,\n      pinSpacing,\n      pins,\n      tail: tailLength,\n      pulse: pulseDuration,\n      draw: drawDuration,\n      drawStagger,\n      trace,\n      packet,\n    });\n  }, [\n    flow,\n    autoPlay,\n    interval,\n    speed,\n    fanout,\n    highlight,\n    radius,\n    pinSpacing,\n    pins,\n    tailLength,\n    pulseDuration,\n    drawDuration,\n    drawStagger,\n    trace,\n    packet,\n  ]);\n\n  React.useImperativeHandle(\n    apiRef,\n    () => ({ send: (node: string) => engineRef.current?.send(node) }),\n    [],\n  );\n\n  return (\n    <div ref={rootRef} data-circuit-root=\"\" className={cn(\"relative isolate\", className)} {...props}>\n      {/* Under every card: in a stacking context of its own, a negative z-index paints\n          above the circuit's background but below its in-flow content. m-0 and border-0\n          keep space-* and divide-* on the circuit from reaching it. */}\n      <svg\n        ref={svgRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 -z-10 m-0 size-full overflow-visible border-0\"\n      />\n      {children}\n    </div>\n  );\n}\n\n/* ─────────────────────────────────────────────────────────\n * FLOW\n * ───────────────────────────────────────────────────────── */\n\nfunction parseFlow(flow: CircuitFlow): CircuitTrace[] {\n  const traces = new Map<string, CircuitTrace>();\n  for (const item of typeof flow === \"string\" ? [flow] : flow) {\n    if (typeof item !== \"string\") {\n      const key = `${item.from}\\n${item.to}`;\n      traces.set(key, { ...traces.get(key), ...item });\n      continue;\n    }\n    for (const chain of item.split(/[\\n;]/)) {\n      const stages = chain\n        .split(\"->\")\n        .map((stage) => stage.split(\",\").map((id) => id.trim()).filter(Boolean));\n      for (let i = 0; i < stages.length - 1; i++) {\n        for (const from of stages[i]) {\n          for (const to of stages[i + 1]) {\n            const key = `${from}\\n${to}`;\n            if (from !== to && !traces.has(key)) traces.set(key, { from, to });\n          }\n        }\n      }\n    }\n  }\n  return [...traces.values()];\n}\n\n/* ─────────────────────────────────────────────────────────\n * GEOMETRY\n * ───────────────────────────────────────────────────────── */\n\ntype Pt = { x: number; y: number };\ntype Box = { l: number; t: number; r: number; b: number };\n\n/** Shortest straight run out of a card before a trace may bend. */\nconst STUB = 12;\n/** How far a detour stays clear of the cards it goes around. */\nconst GUTTER = 18;\n\nconst isHorizontal = (side: Side) => side === \"left\" || side === \"right\";\nconst OPPOSITE: Record<Side, Side> = { top: \"bottom\", bottom: \"top\", left: \"right\", right: \"left\" };\n\nfunction anchor(box: Box, side: Side, offset: number): Pt {\n  const cx = (box.l + box.r) / 2;\n  const cy = (box.t + box.b) / 2;\n  if (side === \"top\") return { x: cx + offset, y: box.t };\n  if (side === \"bottom\") return { x: cx + offset, y: box.b };\n  if (side === \"left\") return { x: box.l, y: cy + offset };\n  return { x: box.r, y: cy + offset };\n}\n\n/** Half the usable length of a card edge, so pins stay clear of its corners. */\nfunction sideReach(box: Box, side: Side) {\n  const length = isHorizontal(side) ? box.b - box.t : box.r - box.l;\n  return Math.max(0, length / 2 - Math.min(10, length / 4));\n}\n\ntype Kind = \"channel\" | \"detour\" | \"elbow\" | \"free\";\n\n/**\n * Right-angle waypoints. A channel route (Z between facing edges, or a\n * detour U around the side) runs its middle leg at `channel`; an elbow\n * bends once; a free route stubs out of both edges and meets in between.\n */\nfunction waypoints(kind: Kind, from: Side, to: Side, start: Pt, end: Pt, channel: number): Pt[] {\n  if (kind === \"channel\" || kind === \"detour\") {\n    return isHorizontal(from)\n      ? [start, { x: channel, y: start.y }, { x: channel, y: end.y }, end]\n      : [start, { x: start.x, y: channel }, { x: end.x, y: channel }, end];\n  }\n  if (kind === \"elbow\") {\n    return isHorizontal(from) ? [start, { x: end.x, y: start.y }, end] : [start, { x: start.x, y: end.y }, end];\n  }\n  const out = step(start, from, STUB);\n  const into = step(end, to, STUB);\n  const middle = isHorizontal(from) ? { x: out.x, y: into.y } : { x: into.x, y: out.y };\n  return [start, out, middle, into, end];\n}\n\nfunction step(point: Pt, side: Side, distance: number): Pt {\n  if (side === \"top\") return { x: point.x, y: point.y - distance };\n  if (side === \"bottom\") return { x: point.x, y: point.y + distance };\n  if (side === \"left\") return { x: point.x - distance, y: point.y };\n  return { x: point.x + distance, y: point.y };\n}\n\n/** Drop repeated and collinear points. */\nfunction simplify(points: Pt[]): Pt[] {\n  const out: Pt[] = [];\n  for (const p of points) {\n    const last = out[out.length - 1];\n    if (last && Math.abs(last.x - p.x) < 0.5 && Math.abs(last.y - p.y) < 0.5) continue;\n    if (out.length >= 2) {\n      const a = out[out.length - 2];\n      if ((Math.abs(a.x - last.x) < 0.5 && Math.abs(last.x - p.x) < 0.5) || (Math.abs(a.y - last.y) < 0.5 && Math.abs(last.y - p.y) < 0.5)) {\n        out[out.length - 1] = p;\n        continue;\n      }\n    }\n    out.push(p);\n  }\n  return out;\n}\n\n/** Does an axis-aligned segment pass through the inside of a box? */\nfunction hits(a: Pt, b: Pt, box: Box) {\n  return Math.max(a.x, b.x) > box.l && Math.min(a.x, b.x) < box.r && Math.max(a.y, b.y) > box.t && Math.min(a.y, b.y) < box.b;\n}\n\n/** Crossings (1 each) and overlapping runs (2 each) between two right-angle polylines. */\nfunction tangles(p: Pt[], q: Pt[]) {\n  let count = 0;\n  for (let i = 0; i < p.length - 1; i++) {\n    for (let j = 0; j < q.length - 1; j++) {\n      const [a, b, c, d] = [p[i], p[i + 1], q[j], q[j + 1]];\n      const pv = Math.abs(a.x - b.x) < 0.5;\n      const qv = Math.abs(c.x - d.x) < 0.5;\n      if (pv === qv) {\n        const same = pv ? Math.abs(a.x - c.x) < 0.5 : Math.abs(a.y - c.y) < 0.5;\n        const lo = pv ? Math.max(Math.min(a.y, b.y), Math.min(c.y, d.y)) : Math.max(Math.min(a.x, b.x), Math.min(c.x, d.x));\n        const hi = pv ? Math.min(Math.max(a.y, b.y), Math.max(c.y, d.y)) : Math.min(Math.max(a.x, b.x), Math.max(c.x, d.x));\n        if (same && hi - lo > 1) count += 2;\n      } else {\n        const [v1, v2, h1, h2] = pv ? [a, b, c, d] : [c, d, a, b];\n        const x = v1.x;\n        const y = h1.y;\n        if (x > Math.min(h1.x, h2.x) + 0.5 && x < Math.max(h1.x, h2.x) - 0.5 && y > Math.min(v1.y, v2.y) + 0.5 && y < Math.max(v1.y, v2.y) - 0.5) {\n          count += 1;\n        }\n      }\n    }\n  }\n  return count;\n}\n\ntype Segment =\n  | { line: true; a: Pt; b: Pt; length: number }\n  | { line: false; c: Pt; r: number; a0: number; turn: number; length: number };\n\ntype Shape = { d: string; segments: Segment[]; length: number; start: Pt; end: Pt };\n\n/** Rounded corners as true quarter arcs, so the length (and every packet position) is exact. */\nfunction shape(points: Pt[], radius: number): Shape {\n  const segments: Segment[] = [];\n  const last = points.length - 1;\n  let d = `M${points[0].x} ${points[0].y}`;\n  let cursor = points[0];\n\n  for (let i = 1; i < last; i++) {\n    const [prev, at, next] = [points[i - 1], points[i], points[i + 1]];\n    const inLength = Math.hypot(at.x - prev.x, at.y - prev.y);\n    const outLength = Math.hypot(next.x - at.x, next.y - at.y);\n    const u = { x: (at.x - prev.x) / inLength, y: (at.y - prev.y) / inLength };\n    const v = { x: (next.x - at.x) / outLength, y: (next.y - at.y) / outLength };\n    const r = Math.min(radius, i === 1 ? inLength : inLength / 2, i + 1 === last ? outLength : outLength / 2);\n    const enter = { x: at.x - u.x * r, y: at.y - u.y * r };\n    const exit = { x: at.x + v.x * r, y: at.y + v.y * r };\n    const line = Math.hypot(enter.x - cursor.x, enter.y - cursor.y);\n    if (line > 0.01) segments.push({ line: true, a: cursor, b: enter, length: line });\n    d += ` L${enter.x} ${enter.y}`;\n    if (r > 0.5) {\n      const cross = u.x * v.y - u.y * v.x;\n      const c = { x: enter.x + v.x * r, y: enter.y + v.y * r };\n      segments.push({\n        line: false,\n        c,\n        r,\n        a0: Math.atan2(enter.y - c.y, enter.x - c.x),\n        turn: cross > 0 ? Math.PI / 2 : -Math.PI / 2,\n        length: (Math.PI / 2) * r,\n      });\n      d += ` A${r} ${r} 0 0 ${cross > 0 ? 1 : 0} ${exit.x} ${exit.y}`;\n    } else {\n      d += ` L${exit.x} ${exit.y}`;\n    }\n    cursor = exit;\n  }\n\n  const end = points[last];\n  const tail = Math.hypot(end.x - cursor.x, end.y - cursor.y);\n  if (tail > 0.01) segments.push({ line: true, a: cursor, b: end, length: tail });\n  d += ` L${end.x} ${end.y}`;\n  const length = segments.reduce((sum, s) => sum + s.length, 0);\n  return { d, segments, length, start: points[0], end };\n}\n\nfunction pointAt(shape: Shape, distance: number): Pt {\n  let left = Math.max(0, Math.min(distance, shape.length));\n  for (const s of shape.segments) {\n    if (left <= s.length) {\n      const k = s.length ? left / s.length : 0;\n      if (s.line) return { x: s.a.x + (s.b.x - s.a.x) * k, y: s.a.y + (s.b.y - s.a.y) * k };\n      const angle = s.a0 + s.turn * k;\n      return { x: s.c.x + Math.cos(angle) * s.r, y: s.c.y + Math.sin(angle) * s.r };\n    }\n    left -= s.length;\n  }\n  return shape.end;\n}\n\n/* ─────────────────────────────────────────────────────────\n * ROUTER\n * ───────────────────────────────────────────────────────── */\n\ntype Plan = {\n  index: number;\n  a: Box;\n  b: Box;\n  from: Side;\n  to: Side;\n  kind: Kind;\n  channel: number;\n  /** Where the middle leg may sit: between the cards for a Z, outward from `lo` for a detour. */\n  lo: number;\n  hi: number;\n  outward: 1 | -1;\n  fromOffset: number;\n  toOffset: number;\n  points: Pt[];\n};\n\nfunction build(plan: Plan) {\n  const start = anchor(plan.a, plan.from, plan.fromOffset);\n  const end = anchor(plan.b, plan.to, plan.toOffset);\n  plan.points = waypoints(plan.kind, plan.from, plan.to, start, end, plan.channel);\n}\n\ntype Candidate = Omit<Plan, \"index\" | \"points\" | \"fromOffset\" | \"toOffset\"> & {\n  /** Taste: how much less we like this shape than a Z along the main axis. */\n  penalty: number;\n};\n\nfunction candidates(trace: CircuitTrace, a: Box, b: Box, obstacles: Box[]): Candidate[] {\n  const out: Candidate[] = [];\n  const gapX = Math.max(b.l - a.r, a.l - b.r);\n  const gapY = Math.max(b.t - a.b, a.t - b.b);\n  const rightward = b.l - a.r >= a.l - b.r;\n  const downward = b.t - a.b >= a.t - b.b;\n  // The main axis, with a lean toward horizontal: gaps that are nearly equal shouldn't\n  // flip a trace's direction (and leave a grid lopsided) over a few px.\n  const across = gapX >= gapY * 0.8;\n\n  // Z between facing edges: the tidy default when the cards sit side by side or one above the other.\n  if (gapX >= 2 * STUB) {\n    const [lo, hi] = rightward ? [a.r + STUB, b.l - STUB] : [b.r + STUB, a.l - STUB];\n    out.push({ a, b, from: rightward ? \"right\" : \"left\", to: rightward ? \"left\" : \"right\", kind: \"channel\", channel: (lo + hi) / 2, lo, hi, outward: 1, penalty: across ? 0 : 60 });\n  }\n  if (gapY >= 2 * STUB) {\n    const [lo, hi] = downward ? [a.b + STUB, b.t - STUB] : [b.b + STUB, a.t - STUB];\n    out.push({ a, b, from: downward ? \"bottom\" : \"top\", to: downward ? \"top\" : \"bottom\", kind: \"channel\", channel: (lo + hi) / 2, lo, hi, outward: 1, penalty: across ? 60 : 0 });\n  }\n\n  // Elbows: one bend, for cards that sit diagonally.\n  const bx = (b.l + b.r) / 2;\n  const by = (b.t + b.b) / 2;\n  const ax = (a.l + a.r) / 2;\n  const ay = (a.t + a.b) / 2;\n  const hFrom: Side = bx >= ax ? \"right\" : \"left\";\n  const hTo: Side = ay <= b.t ? \"top\" : \"bottom\";\n  if ((hFrom === \"right\" ? bx >= a.r + STUB : bx <= a.l - STUB) && (hTo === \"top\" ? ay <= b.t - STUB : ay >= b.b + STUB)) {\n    out.push({ a, b, from: hFrom, to: hTo, kind: \"elbow\", channel: 0, lo: 0, hi: 0, outward: 1, penalty: 80 });\n  }\n  const vFrom: Side = by >= ay ? \"bottom\" : \"top\";\n  const vTo: Side = ax <= b.l ? \"left\" : \"right\";\n  if ((vFrom === \"bottom\" ? by >= a.b + STUB : by <= a.t - STUB) && (vTo === \"left\" ? ax <= b.l - STUB : ax >= b.r + STUB)) {\n    out.push({ a, b, from: vFrom, to: vTo, kind: \"elbow\", channel: 0, lo: 0, hi: 0, outward: 1, penalty: 80 });\n  }\n\n  // Detours: out one side, along a gutter past everything in the way, back in the same side.\n  for (const side of [\"right\", \"left\", \"bottom\", \"top\"] as Side[]) {\n    const horizontal = isHorizontal(side);\n    const outward = side === \"right\" || side === \"bottom\" ? 1 : -1;\n    const span = horizontal ? [Math.min(ay, by), Math.max(ay, by)] : [Math.min(ax, bx), Math.max(ax, bx)];\n    let edge = outward > 0 ? -Infinity : Infinity;\n    for (const box of [a, b, ...obstacles]) {\n      const inSpan = horizontal ? box.b > span[0] && box.t < span[1] : box.r > span[0] && box.l < span[1];\n      if (box !== a && box !== b && !inSpan) continue;\n      const far = side === \"right\" ? box.r : side === \"left\" ? box.l : side === \"bottom\" ? box.b : box.t;\n      edge = outward > 0 ? Math.max(edge, far) : Math.min(edge, far);\n    }\n    const channel = edge + outward * GUTTER;\n    out.push({ a, b, from: side, to: side, kind: \"detour\", channel, lo: channel, hi: channel, outward, penalty: 200 });\n  }\n\n  const fits = out.filter((c) => (!trace.fromSide || c.from === trace.fromSide) && (!trace.toSide || c.to === trace.toSide));\n  if (fits.length) return fits;\n\n  // Sides pinned to something none of the shapes above can do: stub out of both and meet in the middle.\n  const from = trace.fromSide ?? (across ? (rightward ? \"right\" : \"left\") : downward ? \"bottom\" : \"top\");\n  const to = trace.toSide ?? OPPOSITE[from];\n  return [{ a, b, from, to, kind: \"free\", channel: 0, lo: 0, hi: 0, outward: 1, penalty: 0 }];\n}\n\nfunction route(traces: CircuitTrace[], boxes: Map<string, Box>, width: number, height: number, pinSpacing: number): (Pt[] | null)[] {\n  const plans: (Plan | null)[] = traces.map((trace, index) => {\n    const a = boxes.get(trace.from);\n    const b = boxes.get(trace.to);\n    if (!a || !b) return null;\n    const obstacles = [...boxes.values()].filter((box) => box !== a && box !== b);\n    let best: Plan | null = null;\n    let bestCost = Infinity;\n    for (const candidate of candidates(trace, a, b, obstacles)) {\n      const { penalty, ...rest } = candidate;\n      const plan: Plan = { ...rest, index, fromOffset: 0, toOffset: 0, points: [] };\n      build(plan);\n      const cost = score(plan, obstacles, width, height) + penalty;\n      if (cost < bestCost) {\n        best = plan;\n        bestCost = cost;\n      }\n    }\n    return best;\n  });\n  const live = plans.filter((p): p is Plan => p !== null);\n\n  // Pins: traces sharing a card edge spread along it, ordered by where their other end is.\n  const groups = new Map<string, { plan: Plan; end: \"from\" | \"to\"; order: number }[]>();\n  for (const plan of live) {\n    const ends: [\"from\" | \"to\", Side, Box, Side][] = [\n      [\"from\", plan.from, plan.b, plan.to],\n      [\"to\", plan.to, plan.a, plan.from],\n    ];\n    for (const [end, side, other, otherSide] of ends) {\n      const key = `${end === \"from\" ? traces[plan.index].from : traces[plan.index].to}:${side}`;\n      const there = anchor(other, otherSide, 0);\n      const order = isHorizontal(side) ? there.y : there.x;\n      const list = groups.get(key) ?? [];\n      list.push({ plan, end, order: order + plan.index * 1e-3 });\n      groups.set(key, list);\n    }\n  }\n  const single = new Set<string>();\n  for (const list of groups.values()) {\n    list.sort((p, q) => p.order - q.order);\n    const { plan, end } = list[0];\n    const box = end === \"from\" ? plan.a : plan.b;\n    const side = end === \"from\" ? plan.from : plan.to;\n    const reach = sideReach(box, side);\n    const gap = list.length > 1 ? Math.min(pinSpacing, (2 * reach) / (list.length - 1)) : 0;\n    list.forEach((entry, i) => {\n      const offset = (i - (list.length - 1) / 2) * gap;\n      if (entry.end === \"from\") entry.plan.fromOffset = offset;\n      else entry.plan.toOffset = offset;\n    });\n    if (list.length === 1) single.add(`${plan.index}:${end}`);\n  }\n\n  // A Z that would only jog a few px is pulled straight by sliding a lone pin.\n  for (const plan of live) {\n    if (plan.kind !== \"channel\") continue;\n    build(plan);\n    const [start, end] = [plan.points[0], plan.points[plan.points.length - 1]];\n    const drift = isHorizontal(plan.from) ? end.y - start.y : end.x - start.x;\n    if (Math.abs(drift) < 0.5 || Math.abs(drift) > 10) continue;\n    if (single.has(`${plan.index}:from`) && Math.abs(plan.fromOffset + drift) <= sideReach(plan.a, plan.from)) plan.fromOffset += drift;\n    else if (single.has(`${plan.index}:to`) && Math.abs(plan.toOffset - drift) <= sideReach(plan.b, plan.to)) plan.toOffset -= drift;\n  }\n  for (const plan of live) build(plan);\n\n  // Channels: middle legs that would run on top of each other spread apart.\n  const channels = live.filter((p) => p.kind === \"channel\" || p.kind === \"detour\");\n  const span = (p: Plan) => {\n    const [s, e] = [p.points[0], p.points[p.points.length - 1]];\n    return isHorizontal(p.from) ? [Math.min(s.y, e.y), Math.max(s.y, e.y)] : [Math.min(s.x, e.x), Math.max(s.x, e.x)];\n  };\n  const parent = channels.map((_, i) => i);\n  const find = (i: number): number => (parent[i] === i ? i : (parent[i] = find(parent[i])));\n  for (let i = 0; i < channels.length; i++) {\n    for (let j = i + 1; j < channels.length; j++) {\n      const [p, q] = [channels[i], channels[j]];\n      if (isHorizontal(p.from) !== isHorizontal(q.from) || Math.abs(p.channel - q.channel) >= pinSpacing) continue;\n      const [ps, qs] = [span(p), span(q)];\n      if (Math.min(ps[1], qs[1]) - Math.max(ps[0], qs[0]) > -1) parent[find(i)] = find(j);\n    }\n  }\n  const clusters = new Map<number, Plan[]>();\n  channels.forEach((plan, i) => {\n    const list = clusters.get(find(i)) ?? [];\n    list.push(plan);\n    clusters.set(find(i), list);\n  });\n  for (const cluster of clusters.values()) {\n    if (cluster.length < 2) continue;\n    const detour = cluster[0].kind === \"detour\";\n    const outward = cluster[0].outward;\n    const base = detour\n      ? cluster.reduce((m, p) => (outward > 0 ? Math.max(m, p.channel) : Math.min(m, p.channel)), cluster[0].channel)\n      : cluster.reduce((sum, p) => sum + p.channel, 0) / cluster.length;\n    const room = detour ? pinSpacing : Math.max(0, Math.min(pinSpacing, ...cluster.map((p) => (p.hi - p.lo) / (cluster.length - 1))));\n    const slot = (i: number) => (detour ? base + outward * i * room : base + (i - (cluster.length - 1) / 2) * room);\n\n    let bestOrder = cluster;\n    let bestTangles = Infinity;\n    for (const order of orders(cluster)) {\n      order.forEach((plan, i) => {\n        plan.channel = detour ? slot(i) : Math.max(plan.lo, Math.min(plan.hi, slot(i)));\n        build(plan);\n      });\n      let count = 0;\n      for (let i = 0; i < order.length; i++) for (let j = i + 1; j < order.length; j++) count += tangles(order[i].points, order[j].points);\n      if (count < bestTangles) {\n        bestTangles = count;\n        bestOrder = [...order];\n      }\n      if (count === 0) break;\n    }\n    bestOrder.forEach((plan, i) => {\n      plan.channel = detour ? slot(i) : Math.max(plan.lo, Math.min(plan.hi, slot(i)));\n      build(plan);\n    });\n  }\n\n  return plans.map((plan) => (plan ? simplify(plan.points) : null));\n}\n\nfunction score(plan: Plan, obstacles: Box[], width: number, height: number) {\n  const points = simplify(plan.points);\n  const inset = (box: Box, by: number): Box => ({ l: box.l + by, t: box.t + by, r: box.r - by, b: box.b - by });\n  let cost = (points.length - 2) * 20;\n  for (let i = 0; i < points.length - 1; i++) {\n    const [p, q] = [points[i], points[i + 1]];\n    cost += Math.hypot(q.x - p.x, q.y - p.y) * 0.25;\n    for (const box of obstacles) if (hits(p, q, inset(box, -2))) cost += 10000;\n    if (hits(p, q, inset(plan.a, 1)) || hits(p, q, inset(plan.b, 1))) cost += 10000;\n  }\n  for (const p of points) {\n    const outside = Math.max(0, -p.x, p.x - width) + Math.max(0, -p.y, p.y - height);\n    cost += outside * 4;\n  }\n  return cost;\n}\n\n/** Orderings to try for a cluster: all of them while that's cheap, else the two obvious ones. */\nfunction orders(list: Plan[]): Plan[][] {\n  const mid = (p: Plan) => {\n    const [s, e] = [p.points[0], p.points[p.points.length - 1]];\n    return isHorizontal(p.from) ? s.y + e.y : s.x + e.x;\n  };\n  const sorted = [...list].sort((p, q) => mid(p) - mid(q));\n  if (list.length > 5) return [sorted, [...sorted].reverse()];\n  const out: Plan[][] = [];\n  const permute = (rest: Plan[], acc: Plan[]) => {\n    if (!rest.length) out.push(acc);\n    rest.forEach((p, i) => permute([...rest.slice(0, i), ...rest.slice(i + 1)], [...acc, p]));\n  };\n  permute(sorted, []);\n  return out;\n}\n\n/* ─────────────────────────────────────────────────────────\n * ENGINE\n * ───────────────────────────────────────────────────────── */\n\ntype Options = {\n  flow: CircuitFlow;\n  autoPlay: boolean;\n  interval: number;\n  speed: number;\n  fanout: \"all\" | \"one\";\n  highlight: \"path\" | \"neighbors\" | \"none\";\n  radius: number;\n  pinSpacing: number;\n  pins: boolean;\n  /** Px, the packet's tail. */\n  tail: number;\n  /** Seconds a card keeps data-circuit-pulse. */\n  pulse: number;\n  /** Seconds a trace takes to draw in. */\n  draw: number;\n  /** Seconds between stages drawing in on mount. */\n  drawStagger: number;\n  trace: string;\n  packet: string;\n};\n\ntype Engine = {\n  configure: (options: Options) => void;\n  send: (node: string) => void;\n  destroy: () => void;\n};\n\nconst SVG = \"http://www.w3.org/2000/svg\";\n/** Packets a trace can carry at once. */\nconst SLOTS = 3;\n/** Length of a packet's glowing tail, in px; the `tailLength` prop. */\nconst TAIL = 44;\n\ntype Slot = { glow: SVGPathElement; body: SVGPathElement; core: SVGPathElement; head: SVGCircleElement; halo: SVGCircleElement };\ntype Wire = {\n  trace: CircuitTrace;\n  group: SVGGElement;\n  track: SVGPathElement;\n  lit: SVGPathElement;\n  pins: SVGCircleElement[];\n  flash: SVGCircleElement;\n  slots: Slot[];\n  shape: Shape | null;\n  drawFrom: number;\n  flashAt: number;\n};\ntype Packet = { wire: Wire; t0: number; duration: number; hops: number; arrived: boolean };\n\nfunction svg<K extends keyof SVGElementTagNameMap>(tag: K, attrs: Record<string, string | number>, parent: Element) {\n  const node = document.createElementNS(SVG, tag);\n  for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, String(value));\n  parent.appendChild(node);\n  return node;\n}\n\nconst ease = (t: number) => 0.45 * t + 0.55 * (0.5 - Math.cos(Math.PI * t) / 2);\n\nfunction createEngine(root: HTMLDivElement, layer: SVGSVGElement, arrive: (node: string, from: string) => void): Engine {\n  let options: Options | null = null;\n  let flowKey = \"\";\n  let traces: CircuitTrace[] = [];\n  let wires: Wire[] = [];\n  const nodes = new Map<string, HTMLElement>();\n  let boxes = new Map<string, Box>();\n  let lastBoxes = new Float64Array(0);\n  let routed = false;\n  let packets: Packet[] = [];\n  const nextAt = new Map<string, number>();\n  const lastSent = new Map<string, number>();\n  const turn = new Map<string, number>();\n  const pulses = new Map<string, number>();\n  let active: string | null = null;\n  let clearTimer = 0;\n  let visible = false;\n  let raf = 0;\n  let last = 0;\n  let wakeUntil = 0;\n  let firstDraw = -1;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  const now = () => performance.now() / 1000;\n\n  /* nodes */\n\n  function scan() {\n    nodes.clear();\n    root.querySelectorAll<HTMLElement>(\"[data-circuit]\").forEach((node) => {\n      const id = node.dataset.circuit;\n      if (!id || nodes.has(id) || node.closest(\"[data-circuit-root]\") !== root) return;\n      nodes.set(id, node);\n    });\n    resize.disconnect();\n    resize.observe(root);\n    nodes.forEach((node) => resize.observe(node));\n    lastBoxes = new Float64Array(0);\n    wake();\n  }\n\n  /** Read every node box relative to the circuit; true if anything moved. */\n  function measure() {\n    const frame = root.getBoundingClientRect();\n    const sx = root.offsetWidth ? frame.width / root.offsetWidth : 1;\n    const sy = root.offsetHeight ? frame.height / root.offsetHeight : 1;\n    const ox = frame.left + root.clientLeft * sx;\n    const oy = frame.top + root.clientTop * sy;\n    const next = new Map<string, Box>();\n    const flat = new Float64Array(nodes.size * 4 + 2);\n    flat[0] = root.clientWidth;\n    flat[1] = root.clientHeight;\n    let i = 2;\n    nodes.forEach((node, id) => {\n      const r = node.getBoundingClientRect();\n      if (!r.width && !r.height) return;\n      const box = { l: (r.left - ox) / sx, t: (r.top - oy) / sy, r: (r.right - ox) / sx, b: (r.bottom - oy) / sy };\n      next.set(id, box);\n      flat[i++] = box.l;\n      flat[i++] = box.t;\n      flat[i++] = box.r;\n      flat[i++] = box.b;\n    });\n    boxes = next;\n    let moved = flat.length !== lastBoxes.length;\n    for (let j = 0; !moved && j < flat.length; j++) moved = Math.abs(flat[j] - lastBoxes[j]) > 0.25;\n    if (moved) lastBoxes = flat;\n    return moved;\n  }\n\n  /* wires */\n\n  function rebuild() {\n    if (!options) return;\n    layer.replaceChildren();\n    packets = [];\n    wires = traces.map((trace) => {\n      const group = svg(\"g\", { class: `transition-opacity ${TIMING.highlight}` }, layer);\n      const track = svg(\"path\", { fill: \"none\", stroke: \"currentColor\", \"stroke-width\": 1.25, class: options!.trace }, group);\n      const pinGroup = svg(\"g\", { class: options!.trace, fill: \"currentColor\" }, group);\n      const pins = [svg(\"circle\", { r: 2.75, opacity: 0 }, pinGroup), svg(\"circle\", { r: 2.75, opacity: 0 }, pinGroup)];\n      const paint = svg(\"g\", { class: options!.packet, fill: \"none\", stroke: \"currentColor\", \"stroke-linecap\": \"round\" }, group);\n      if (trace.color) paint.style.color = trace.color;\n      const lit = svg(\"path\", { \"stroke-width\": 1.25, opacity: 0, class: `transition-opacity ${TIMING.highlight}` }, paint);\n      const flash = svg(\"circle\", { r: 2.75, fill: \"currentColor\", stroke: \"none\", opacity: 0 }, paint);\n      const slots = Array.from({ length: SLOTS }, () => ({\n        glow: svg(\"path\", { \"stroke-width\": 7, opacity: 0, \"stroke-opacity\": 0.16 }, paint),\n        body: svg(\"path\", { \"stroke-width\": 1.5, opacity: 0, \"stroke-opacity\": 0.55 }, paint),\n        core: svg(\"path\", { \"stroke-width\": 2, opacity: 0 }, paint),\n        halo: svg(\"circle\", { r: 6, fill: \"currentColor\", stroke: \"none\", opacity: 0, \"fill-opacity\": 0.2 }, paint),\n        head: svg(\"circle\", { r: 2.25, fill: \"currentColor\", stroke: \"none\", opacity: 0 }, paint),\n      }));\n      return { trace, group, track, lit, pins, flash, slots, shape: null, drawFrom: -1, flashAt: -Infinity };\n    });\n    lastBoxes = new Float64Array(0);\n    applyHighlight();\n    wake();\n  }\n\n  function reroute(t: number) {\n    if (!options) return;\n    const points = route(traces, boxes, root.clientWidth, root.clientHeight, options.pinSpacing);\n    const depth = depths();\n    wires.forEach((wire, i) => {\n      const p = points[i];\n      if (!p || p.length < 2) {\n        wire.shape = null;\n        wire.group.style.display = \"none\";\n        return;\n      }\n      wire.group.style.display = \"\";\n      const s = shape(p, options!.radius);\n      wire.shape = s;\n      for (const path of [wire.track, wire.lit, ...wire.slots.flatMap((slot) => [slot.glow, slot.body, slot.core])]) {\n        path.setAttribute(\"d\", s.d);\n        path.setAttribute(\"pathLength\", String(s.length));\n      }\n      wire.pins[0].setAttribute(\"cx\", String(s.start.x));\n      wire.pins[0].setAttribute(\"cy\", String(s.start.y));\n      wire.pins[1].setAttribute(\"cx\", String(s.end.x));\n      wire.pins[1].setAttribute(\"cy\", String(s.end.y));\n      wire.flash.setAttribute(\"cx\", String(s.end.x));\n      wire.flash.setAttribute(\"cy\", String(s.end.y));\n      if (wire.drawFrom < 0) {\n        if (firstDraw < 0) firstDraw = t;\n        // Traces present at mount draw in stage by stage; ones that show up later draw on their own.\n        wire.drawFrom = t - firstDraw < 0.1 ? t + (depth.get(wire.trace.from) ?? 0) * options!.drawStagger : t;\n      }\n    });\n    routed = true;\n  }\n\n  function depths() {\n    const depth = new Map<string, number>();\n    const incoming = new Set(traces.map((t) => t.to));\n    let frontier = [...new Set(traces.map((t) => t.from))].filter((id) => !incoming.has(id));\n    frontier.forEach((id) => depth.set(id, 0));\n    for (let level = 1; frontier.length && level < 32; level++) {\n      const next: string[] = [];\n      for (const t of traces) {\n        if (frontier.includes(t.from) && !depth.has(t.to)) {\n          depth.set(t.to, level);\n          next.push(t.to);\n        }\n      }\n      frontier = next;\n    }\n    return depth;\n  }\n\n  /* signals */\n\n  const live = (wire: Wire) => wire.shape !== null;\n  const outOf = (node: string) => wires.filter((w) => w.trace.from === node && live(w));\n\n  function emit(node: string, t: number, hops: number, delay: number) {\n    if (!options) return;\n    let out = outOf(node);\n    if (!out.length) return;\n    if (options.fanout === \"one\") {\n      const i = turn.get(node) ?? 0;\n      turn.set(node, i + 1);\n      out = [out[i % out.length]];\n    }\n    lastSent.set(node, t);\n    for (const wire of out) {\n      const length = wire.shape!.length;\n      const mine = packets.filter((p) => p.wire === wire);\n      if (mine.length >= SLOTS) packets.splice(packets.indexOf(mine[0]), 1);\n      packets.push({ wire, t0: t + delay, duration: Math.min(TIMING.trip.max, Math.max(TIMING.trip.min, length / options.speed)), hops, arrived: false });\n    }\n  }\n\n  function land(wire: Wire, t: number, hops: number) {\n    const { from, to } = wire.trace;\n    wire.flashAt = t;\n    pulse(to, t);\n    arrive(to, from);\n    if (hops < 24 && t - (lastSent.get(to) ?? -Infinity) > TIMING.cooldown) emit(to, t, hops + 1, TIMING.relay);\n  }\n\n  function pulse(node: string, t: number) {\n    const element = nodes.get(node);\n    if (!element) return;\n    element.setAttribute(\"data-circuit-pulse\", \"\");\n    pulses.set(node, t + (options?.pulse ?? TIMING.pulseDuration));\n  }\n\n  function send(node: string) {\n    const t = now();\n    if (!options) return;\n    if (!outOf(node).length) {\n      pulse(node, t);\n      wake(options.pulse + TIMING.pastPulse);\n      return;\n    }\n    if (reduced.matches) {\n      // No packets to watch: the signal lands everywhere it would have gone, at once.\n      const seen = new Set([node]);\n      let frontier = [node];\n      while (frontier.length) {\n        const next: string[] = [];\n        for (const id of frontier) {\n          let out = outOf(id);\n          if (options.fanout === \"one\" && out.length) {\n            const i = turn.get(id) ?? 0;\n            turn.set(id, i + 1);\n            out = [out[i % out.length]];\n          }\n          for (const wire of out) {\n            pulse(wire.trace.to, t);\n            arrive(wire.trace.to, id);\n            if (!seen.has(wire.trace.to)) {\n              seen.add(wire.trace.to);\n              next.push(wire.trace.to);\n            }\n          }\n        }\n        frontier = next;\n      }\n      wake(options.pulse + TIMING.pastPulse);\n      return;\n    }\n    emit(node, t, 0, 0);\n    wake();\n  }\n\n  /* frame */\n\n  function frame(ms: number) {\n    raf = 0;\n    const t = ms / 1000;\n    // Back from a hidden tab or off screen: pause time instead of replaying it.\n    if (last && t - last > 0.25) {\n      const skip = t - last - 1 / 60;\n      for (const p of packets) p.t0 += skip;\n      nextAt.forEach((at, id) => nextAt.set(id, at + skip));\n      for (const w of wires) if (w.drawFrom > 0) w.drawFrom += skip;\n    }\n    last = t;\n\n    if (measure() || !routed) reroute(t);\n    const motion = !reduced.matches;\n    let busy = false;\n\n    for (const wire of wires) {\n      const s = wire.shape;\n      if (!s) continue;\n      // Draw-in.\n      const p = motion ? Math.min(1, Math.max(0, (t - wire.drawFrom) / (options?.draw ?? TIMING.drawDuration))) : 1;\n      if (p < 1) {\n        busy = true;\n        wire.track.setAttribute(\"stroke-dasharray\", `${s.length} ${s.length + 1}`);\n        wire.track.setAttribute(\"stroke-dashoffset\", String(s.length * (1 - ease(p))));\n      } else if (wire.track.hasAttribute(\"stroke-dasharray\")) {\n        wire.track.removeAttribute(\"stroke-dasharray\");\n        wire.track.removeAttribute(\"stroke-dashoffset\");\n      }\n      const pinsOn = options?.pins ? 1 : 0;\n      wire.pins[0].setAttribute(\"opacity\", String(pinsOn * Math.min(1, p * 4)));\n      wire.pins[1].setAttribute(\"opacity\", String(pinsOn * Math.max(0, p * 4 - 3)));\n      // Landing flash on the pin.\n      const f = (t - wire.flashAt) / TIMING.flash;\n      if (f >= 0 && f < 1) {\n        busy = true;\n        wire.flash.setAttribute(\"opacity\", String(pinsOn * (1 - f)));\n        wire.flash.setAttribute(\"r\", String(2.75 + 3 * ease(f)));\n      } else wire.flash.setAttribute(\"opacity\", \"0\");\n    }\n\n    if (motion && options) {\n      // Sources send on their own once their traces are drawn.\n      if (options.autoPlay) {\n        const sources = sourceList();\n        sources.forEach((id, i) => {\n          const ready = Math.max(...outOf(id).map((w) => w.drawFrom + options!.draw));\n          if (!nextAt.has(id)) nextAt.set(id, ready + (i * options!.interval) / sources.length);\n          let at = nextAt.get(id)!;\n          if (t >= at) {\n            emit(id, at, 0, 0);\n            at = Math.max(at + options!.interval * (0.85 + Math.random() * 0.3), t);\n            nextAt.set(id, at);\n          }\n        });\n        busy = busy || sources.length > 0;\n      }\n      drawPackets(t);\n      busy = busy || packets.length > 0;\n    } else {\n      packets = [];\n      drawPackets(t);\n    }\n\n    pulses.forEach((until, id) => {\n      if (t < until) {\n        busy = true;\n        return;\n      }\n      nodes.get(id)?.removeAttribute(\"data-circuit-pulse\");\n      pulses.delete(id);\n    });\n\n    if (visible && (busy || t < wakeUntil)) raf = requestAnimationFrame(frame);\n    else last = 0;\n  }\n\n  function sourceList() {\n    const fed = new Set(wires.filter(live).map((w) => w.trace.to));\n    const senders = [...new Set(wires.filter(live).map((w) => w.trace.from))];\n    const sources = senders.filter((id) => !fed.has(id));\n    return sources.length ? sources : senders.slice(0, 1);\n  }\n\n  function drawPackets(t: number) {\n    const used = new Map<Wire, number>();\n    const keep: Packet[] = [];\n    for (const p of packets) {\n      const s = p.wire.shape;\n      if (!s) continue;\n      const k = (t - p.t0) / p.duration;\n      if (k < 0) {\n        keep.push(p);\n        continue;\n      }\n      const length = s.length;\n      const most = options?.tail ?? TAIL;\n      const tail = Math.min(most, length * 0.6);\n      // The head eases to the far pin, then the tail drains into the card at full speed.\n      const head = k <= 1 ? length * ease(k) : length + (k - 1) * p.duration * (options?.speed ?? TIMING.speed);\n      if (k >= 1 && !p.arrived) {\n        p.arrived = true;\n        land(p.wire, t, p.hops);\n      }\n      if (head - tail >= length) continue;\n      keep.push(p);\n      const slotIndex = used.get(p.wire) ?? 0;\n      used.set(p.wire, slotIndex + 1);\n      const slot = p.wire.slots[slotIndex];\n      if (!slot) continue;\n      const from = Math.max(0, head - tail);\n      const to = Math.min(head, length);\n      const dash = (start: number, end: number, path: SVGPathElement) => {\n        path.setAttribute(\"stroke-dasharray\", `${Math.max(0, end - start)} ${length + most * 2}`);\n        path.setAttribute(\"stroke-dashoffset\", String(-start));\n        path.setAttribute(\"opacity\", end - start > 0.5 ? \"1\" : \"0\");\n      };\n      dash(from, to, slot.glow);\n      dash(from, to, slot.body);\n      dash(Math.max(from, head - tail * 0.35), to, slot.core);\n      const at = pointAt(s, to);\n      const fade = head > length ? Math.max(0, 1 - (head - length) / (tail * 0.5)) : Math.min(1, head / 8);\n      for (const dot of [slot.head, slot.halo]) {\n        dot.setAttribute(\"cx\", String(at.x));\n        dot.setAttribute(\"cy\", String(at.y));\n        dot.setAttribute(\"opacity\", String(fade));\n      }\n    }\n    packets = keep;\n    for (const wire of wires) {\n      for (let i = used.get(wire) ?? 0; i < SLOTS; i++) {\n        const slot = wire.slots[i];\n        for (const part of [slot.glow, slot.body, slot.core, slot.head, slot.halo]) part.setAttribute(\"opacity\", \"0\");\n      }\n    }\n  }\n\n  function wake(seconds: number = TIMING.awake) {\n    wakeUntil = Math.max(wakeUntil, now() + seconds);\n    if (!raf && visible) raf = requestAnimationFrame(frame);\n  }\n\n  /* highlight */\n\n  function applyHighlight() {\n    const mode = options?.highlight ?? \"path\";\n    const lit = new Set<Wire>();\n    const linked = new Set<string>();\n    if (active && mode !== \"none\") {\n      linked.add(active);\n      const walk = (forward: boolean) => {\n        let frontier = [active!];\n        const seen = new Set(frontier);\n        while (frontier.length) {\n          const next: string[] = [];\n          for (const wire of wires) {\n            const [here, there] = forward ? [wire.trace.from, wire.trace.to] : [wire.trace.to, wire.trace.from];\n            if (!frontier.includes(here)) continue;\n            lit.add(wire);\n            linked.add(there);\n            if (!seen.has(there) && mode === \"path\") {\n              seen.add(there);\n              next.push(there);\n            }\n          }\n          frontier = next;\n        }\n      };\n      walk(true);\n      walk(false);\n    }\n    for (const wire of wires) {\n      wire.group.style.opacity = active && mode !== \"none\" && !lit.has(wire) ? \"0.25\" : \"\";\n      wire.lit.setAttribute(\"opacity\", lit.has(wire) ? \"0.5\" : \"0\");\n    }\n    nodes.forEach((node, id) => {\n      if (!active || mode === \"none\") node.removeAttribute(\"data-circuit-state\");\n      else node.setAttribute(\"data-circuit-state\", id === active ? \"active\" : linked.has(id) ? \"linked\" : \"dimmed\");\n    });\n  }\n\n  function setActive(node: string | null) {\n    window.clearTimeout(clearTimer);\n    if (node === null) {\n      // A short grace period, so crossing the gap between two cards doesn't flash everything back.\n      clearTimer = window.setTimeout(() => {\n        active = null;\n        applyHighlight();\n      }, TIMING.grace);\n      return;\n    }\n    if (node === active) return;\n    active = node;\n    applyHighlight();\n    if (now() - (lastSent.get(node) ?? -Infinity) > TIMING.resend) send(node);\n  }\n\n  function nodeOf(target: EventTarget | null) {\n    if (!(target instanceof Element)) return null;\n    const element = target.closest<HTMLElement>(\"[data-circuit]\");\n    const id = element?.dataset.circuit;\n    return id && nodes.get(id) === element ? id : null;\n  }\n\n  const onOver = (e: PointerEvent) => {\n    if (e.pointerType !== \"touch\") setActive(nodeOf(e.target));\n  };\n  const onLeave = (e: PointerEvent) => {\n    if (e.pointerType !== \"touch\") setActive(null);\n  };\n  const onDown = (e: PointerEvent) => {\n    if (e.pointerType !== \"touch\") return;\n    const node = root.contains(e.target as Node) ? nodeOf(e.target) : null;\n    if (node && node === active) setActive(null);\n    else setActive(node);\n  };\n  const onFocusIn = (e: FocusEvent) => {\n    const node = nodeOf(e.target);\n    if (node) setActive(node);\n  };\n  const onFocusOut = (e: FocusEvent) => {\n    if (!nodeOf(e.relatedTarget)) setActive(null);\n  };\n\n  /* observers */\n\n  const resize = new ResizeObserver(() => wake());\n  const mutations = new MutationObserver((records) => {\n    if (records.some((r) => !layer.contains(r.target) && r.target !== layer)) scan();\n  });\n  mutations.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: [\"data-circuit\"] });\n  const sight = new IntersectionObserver(([entry]) => {\n    visible = entry.isIntersecting;\n    if (visible) wake();\n  });\n  sight.observe(root);\n  const onAnyChange = () => wake();\n  root.addEventListener(\"transitionrun\", onAnyChange);\n  root.addEventListener(\"animationstart\", onAnyChange);\n  root.addEventListener(\"pointerover\", onOver);\n  root.addEventListener(\"pointerleave\", onLeave);\n  root.addEventListener(\"focusin\", onFocusIn);\n  root.addEventListener(\"focusout\", onFocusOut);\n  document.addEventListener(\"pointerdown\", onDown);\n  window.addEventListener(\"resize\", onAnyChange);\n  reduced.addEventListener(\"change\", onAnyChange);\n  document.fonts?.ready.then(() => wake());\n\n  scan();\n\n  return {\n    configure(next) {\n      const key = JSON.stringify(next.flow);\n      const restyle = !options || options.trace !== next.trace || options.packet !== next.packet;\n      const reshape = options && (options.radius !== next.radius || options.pinSpacing !== next.pinSpacing);\n      options = next;\n      if (key !== flowKey || restyle) {\n        flowKey = key;\n        traces = parseFlow(next.flow);\n        routed = false;\n        rebuild();\n      } else if (reshape) {\n        routed = false;\n      }\n      if (!next.autoPlay) nextAt.clear();\n      applyHighlight();\n      wake();\n    },\n    send,\n    destroy() {\n      cancelAnimationFrame(raf);\n      window.clearTimeout(clearTimer);\n      resize.disconnect();\n      mutations.disconnect();\n      sight.disconnect();\n      root.removeEventListener(\"transitionrun\", onAnyChange);\n      root.removeEventListener(\"animationstart\", onAnyChange);\n      root.removeEventListener(\"pointerover\", onOver);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      root.removeEventListener(\"focusin\", onFocusIn);\n      root.removeEventListener(\"focusout\", onFocusOut);\n      document.removeEventListener(\"pointerdown\", onDown);\n      window.removeEventListener(\"resize\", onAnyChange);\n      reduced.removeEventListener(\"change\", onAnyChange);\n      nodes.forEach((node) => {\n        node.removeAttribute(\"data-circuit-state\");\n        node.removeAttribute(\"data-circuit-pulse\");\n      });\n      layer.replaceChildren();\n    },\n  };\n}\n"
    }
  ]
}