{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "route-trace",
  "title": "Route trace",
  "description": "Route trace is a production-ready charts component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/charts/route-trace.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type CSSProperties,\n  type KeyboardEvent,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { useElementSize } from \"@/components/bjork-ui/_core/canvas\";\nimport { hashString } from \"@/components/bjork-ui/_core/random\";\nimport { ChartAnnouncer, chartFocusRing, chartVars, type AnnouncerHandle } from \"@/components/bjork-ui/charts/_kit/chrome\";\n\nexport type RouteStatus = \"ok\" | \"degraded\" | \"down\";\n\nexport interface RouteNode {\n  id: string;\n  label: string;\n  sublabel?: string;\n  col: number;\n  row: number;\n  status?: RouteStatus;\n}\n\nexport interface RouteEdge {\n  from: string;\n  to: string;\n  throughput: number;\n  status?: RouteStatus;\n}\n\nexport type RouteOrientation = \"auto\" | \"horizontal\" | \"vertical\";\n\nexport interface RouteTraceProps {\n  /** Nodes on a grid: `col` is the flow step (left to right), `row` the lane within it. */\n  nodes: RouteNode[];\n  /** Directed edges; `throughput` is 0 to 1 and sets packet length and count. */\n  edges: RouteEdge[];\n  /** Grid cell in px for the horizontal layout. */\n  cell?: { w: number; h: number };\n  cornerRadius?: number;\n  speed?: number;\n  selectedId?: string;\n  onNodeSelect?: (id: string) => void;\n  /**\n   * `auto` (default) flows left to right while that fits at a legible size and turns the flow\n   * top to bottom on narrow containers. Either way the whole graph scales to fit, never clips.\n   */\n  orientation?: RouteOrientation;\n  /** One line under the diagram: the status summary, or the active node's traffic. Default true. */\n  readout?: boolean;\n  ariaLabel?: string;\n  tone?: BjorkTone;\n  attract?: boolean;\n  /** Pins the packets at their phase offsets and stops the attract loop. Used for posed previews. */\n  frozen?: boolean;\n  className?: string;\n}\n\nexport interface RouteGeometry {\n  key: string;\n  from: string;\n  to: string;\n  d: string;\n  length: number;\n}\n\nexport interface RoutePin {\n  id: string;\n  side: \"in\" | \"out\";\n  y: number;\n}\n\ninterface Pt {\n  x: number;\n  y: number;\n}\n\ninterface Cell {\n  w: number;\n  h: number;\n}\n\ninterface Resolved {\n  nodes: RouteNode[];\n  byId: Map<string, RouteNode>;\n  /** Flow-space centres: x along the flow (columns), y across it (rows). */\n  centres: Map<string, Pt>;\n  /** Screen centres, after the layout's orientation. */\n  screen: Map<string, Pt>;\n}\n\n// Routing runs in flow space (x = flow, y = lane) and the result is transposed for the vertical layout.\ninterface Layout {\n  vertical: boolean;\n  cell: Cell;\n  /** Node half extent along the flow. */\n  halfFlow: number;\n}\n\nconst DEFAULT_CELL: Cell = { w: 140, h: 88 };\nconst NODE_HALF_W = 48;\nconst NODE_HALF_H = 20;\nconst PIN_SPACING = 8;\nconst LANE_SPACING = 6;\nconst PAD = 24;\nconst MIN_SCALE = 0.84; // a 12px label stays at 10px or more\nconst VERTICAL_CROSS = NODE_HALF_W * 2 + 20; // column pitch when the flow runs top to bottom\nconst MAX_SCALE = 1.25;\nconst PACKET_SPEED = 80;\nconst MIN_PACKET_GAP = 4;\nconst MAX_PACKETS = 4;\nconst ATTRACT_TICK_MS = 250;\nconst ATTRACT_PERIOD_MS = 5000;\nconst ATTRACT_DOWN_MS = 2500;\nconst ATTRACT_IDLE_MS = 4000;\nconst ATTRACT_DRIFT = 0.1;\nconst DIM_EDGE = 0.4;\nconst DIM_NODE = 0.6;\nconst DOWN_EDGE = 0.35;\n\nconst STATUS_WORD: Record<RouteStatus, string> = { ok: \"healthy\", degraded: \"degraded\", down: \"down\" };\nconst ARROW_DIRS: Record<string, Pt> = {\n  ArrowLeft: { x: -1, y: 0 },\n  ArrowRight: { x: 1, y: 0 },\n  ArrowUp: { x: 0, y: -1 },\n  ArrowDown: { x: 0, y: 1 },\n};\n// Hair at 1.8x alpha for the trace, and a 0.05 alpha underlay for the active glow.\nconst TRACE_COLOUR: Record<BjorkTone, { trace: string; glow: string }> = {\n  dark: { trace: \"rgba(237,237,237,0.22)\", glow: \"rgba(237,237,237,0.05)\" },\n  light: { trace: \"rgba(23,23,23,0.22)\", glow: \"rgba(23,23,23,0.05)\" },\n};\n\nconst ROUTE_CSS = `\n@keyframes bjork-route-packet {\n  from { stroke-dashoffset: 0px; }\n  to { stroke-dashoffset: calc(-1 * var(--rt-len, 0px)); }\n}\n[data-route-trace][data-loop=\"paused\"] .bjork-route-packet { animation-play-state: paused !important; }\n`;\n\nconst fmt = (n: number) => String(Math.round(n * 100) / 100);\n// Snap to the half pixel so 1.5px strokes sit on a crisp centre line.\nconst snap = (v: number) => Math.round(v - 0.5) + 0.5;\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst edgeKey = (e: { from: string; to: string }) => `${e.from}->${e.to}`;\n\nconst subscribeNoop = () => () => {};\n\n// False on the server and during hydration, so reduced-motion styles never cause a mismatch.\nfunction useHasMounted() {\n  return useSyncExternalStore(subscribeNoop, () => true, () => false);\n}\n\n// Called only from event handlers, never during render.\nfunction stampInput(ref: { current: number }) {\n  ref.current = performance.now();\n}\n\nfunction devWarn(message: string) {\n  if (process.env.NODE_ENV !== \"production\") console.warn(`RouteTrace: ${message}`);\n}\n\nfunction devError(message: string) {\n  if (process.env.NODE_ENV !== \"production\") console.error(`RouteTrace: ${message}`);\n}\n\nfunction resolveNodes(input: RouteNode[], cell: Cell, vertical = false): Resolved {\n  const nodes: RouteNode[] = [];\n  const byId = new Map<string, RouteNode>();\n  const centres = new Map<string, Pt>();\n  const screen = new Map<string, Pt>();\n  const cells = new Map<string, string>();\n\n  for (const n of input) {\n    const cellKey = `${n.col},${n.row}`;\n    const clash = cells.get(cellKey);\n    if (clash) {\n      devError(`\"${n.id}\" shares cell ${cellKey} with \"${clash}\". Only the first is rendered.`);\n      continue;\n    }\n    if (byId.has(n.id)) {\n      devError(`duplicate node id \"${n.id}\". Only the first is rendered.`);\n      continue;\n    }\n    cells.set(cellKey, n.id);\n    byId.set(n.id, n);\n    nodes.push(n);\n    const c = { x: n.col * cell.w + cell.w / 2, y: n.row * cell.h + cell.h / 2 };\n    centres.set(n.id, c);\n    screen.set(n.id, vertical ? { x: c.y, y: c.x } : c);\n  }\n\n  return { nodes, byId, centres, screen };\n}\n\n// Removes duplicate and collinear points, then fillets every corner with an arc.\n// Every command is M, H, V or A.\nfunction orthoPath(points: Pt[], radius: number): { d: string; length: number } {\n  const dedup: Pt[] = [];\n  for (const p of points) {\n    const last = dedup[dedup.length - 1];\n    if (!last || Math.abs(last.x - p.x) > 0.01 || Math.abs(last.y - p.y) > 0.01) dedup.push(p);\n  }\n\n  const pts: Pt[] = [];\n  for (let i = 0; i < dedup.length; i++) {\n    const p = dedup[i];\n    if (i > 0 && i < dedup.length - 1) {\n      const a = dedup[i - 1];\n      const b = dedup[i + 1];\n      const straight =\n        (Math.abs(a.x - p.x) < 0.01 && Math.abs(p.x - b.x) < 0.01) ||\n        (Math.abs(a.y - p.y) < 0.01 && Math.abs(p.y - b.y) < 0.01);\n      if (straight) continue;\n    }\n    pts.push(p);\n  }\n\n  if (pts.length < 2) return { d: \"\", length: 0 };\n\n  const dist = (a: Pt, b: Pt) => Math.hypot(b.x - a.x, b.y - a.y);\n  const unit = (a: Pt, b: Pt) => {\n    const len = dist(a, b) || 1;\n    return { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n  };\n  const lineTo = (a: Pt, b: Pt) =>\n    Math.abs(a.y - b.y) < 0.01 ? `H${fmt(b.x)}` : `V${fmt(b.y)}`;\n\n  let d = `M${fmt(pts[0].x)} ${fmt(pts[0].y)}`;\n  let pen = pts[0];\n  let length = 0;\n\n  for (let i = 1; i < pts.length; i++) {\n    const p = pts[i];\n    if (i === pts.length - 1) {\n      d += lineTo(pen, p);\n      length += dist(pen, p);\n      break;\n    }\n    const prev = pts[i - 1];\n    const next = pts[i + 1];\n    const rr = Math.min(radius, dist(prev, p) / 2, dist(p, next) / 2);\n    if (rr < 0.5) {\n      d += lineTo(pen, p);\n      length += dist(pen, p);\n      pen = p;\n      continue;\n    }\n    const dIn = unit(prev, p);\n    const dOut = unit(p, next);\n    const entry = { x: p.x - dIn.x * rr, y: p.y - dIn.y * rr };\n    const exit = { x: p.x + dOut.x * rr, y: p.y + dOut.y * rr };\n    const sweep = dIn.x * dOut.y - dIn.y * dOut.x > 0 ? 1 : 0;\n    d += `${lineTo(pen, entry)}A${fmt(rr)} ${fmt(rr)} 0 0 ${sweep} ${fmt(exit.x)} ${fmt(exit.y)}`;\n    length += dist(pen, entry) + (Math.PI / 2) * rr;\n    pen = exit;\n  }\n\n  return { d, length };\n}\n\ninterface RouteBuild {\n  routes: RouteGeometry[];\n  pins: RoutePin[];\n}\n\n// Deterministic orthogonal routing. Adjacent columns use a Z-route. Everything else uses a gutter route.\nfunction buildRoutes(resolved: Resolved, edges: RouteEdge[], cell: Cell, radius: number, halfFlow = NODE_HALF_W, vertical = false): RouteBuild {\n  const { byId, centres } = resolved;\n  const centre = (id: string): Pt => centres.get(id) ?? { x: 0, y: 0 };\n  const colOf = (id: string) => byId.get(id)?.col ?? 0;\n  const rowOf = (id: string) => byId.get(id)?.row ?? 0;\n\n  const valid: RouteEdge[] = [];\n  const seen = new Set<string>();\n  for (const e of edges) {\n    if (!byId.has(e.from) || !byId.has(e.to)) {\n      devWarn(`skipped edge ${e.from} to ${e.to}, because a node is unknown.`);\n      continue;\n    }\n    if (e.from === e.to) {\n      devWarn(`skipped self-loop on \"${e.from}\".`);\n      continue;\n    }\n    const key = edgeKey(e);\n    if (seen.has(key)) continue;\n    seen.add(key);\n    valid.push(e);\n  }\n\n  // Pins. Edges leaving a node sit on its right edge, sorted by target height and spread 8px apart.\n  const pinY = new Map<string, number>();\n  const spread = (owner: \"from\" | \"to\", other: \"from\" | \"to\", side: \"in\" | \"out\") => {\n    const groups = new Map<string, RouteEdge[]>();\n    for (const e of valid) {\n      const list = groups.get(e[owner]) ?? [];\n      list.push(e);\n      groups.set(e[owner], list);\n    }\n    for (const [id, list] of groups) {\n      const sorted = [...list].sort(\n        (a, b) => centre(a[other]).y - centre(b[other]).y || edgeKey(a).localeCompare(edgeKey(b)),\n      );\n      const mid = (sorted.length - 1) / 2;\n      sorted.forEach((e, i) => {\n        const y = snap(centre(id).y + (i - mid) * PIN_SPACING);\n        pinY.set(`${side}|${edgeKey(e)}`, y);\n      });\n    }\n  };\n  spread(\"from\", \"to\", \"out\");\n  spread(\"to\", \"from\", \"in\");\n\n  // Channels and gutters, and which edges share each one, so lanes stay apart.\n  type Plan = { edge: RouteEdge; key: string; zig: boolean; gy: number; groups: string[] };\n  const plans: Plan[] = valid.map((e) => {\n    const sc = colOf(e.from);\n    const tc = colOf(e.to);\n    const sr = rowOf(e.from);\n    const tr = rowOf(e.to);\n    const key = edgeKey(e);\n    if (tc === sc + 1) {\n      return { edge: e, key, zig: true, gy: 0, groups: [`v${sc + 1}`] };\n    }\n    // The gutter next to the target row, on the source's side.\n    const gy = tr > sr ? tr * cell.h : (tr + 1) * cell.h;\n    return { edge: e, key, zig: false, gy, groups: [`v${sc + 1}`, `v${tc}`, `g${gy}`] };\n  });\n\n  const members = new Map<string, { key: string; y: number }[]>();\n  for (const p of plans) {\n    const y = (centre(p.edge.from).y + centre(p.edge.to).y) / 2;\n    for (const g of p.groups) {\n      const list = members.get(g) ?? [];\n      list.push({ key: p.key, y });\n      members.set(g, list);\n    }\n  }\n  const lane = (group: string, key: string): number => {\n    const list = [...(members.get(group) ?? [])].sort((a, b) => a.y - b.y || a.key.localeCompare(b.key));\n    const index = list.findIndex((m) => m.key === key);\n    return (index - (list.length - 1) / 2) * LANE_SPACING;\n  };\n\n  const routes: RouteGeometry[] = plans.map((p) => {\n    const e = p.edge;\n    const s = centre(e.from);\n    const t = centre(e.to);\n    const sc = colOf(e.from);\n    const tc = colOf(e.to);\n    const pOut: Pt = { x: s.x + halfFlow, y: pinY.get(`out|${p.key}`) ?? s.y };\n    const pIn: Pt = { x: t.x - halfFlow, y: pinY.get(`in|${p.key}`) ?? t.y };\n\n    let raw: Pt[];\n    if (p.zig) {\n      const b = sc + 1;\n      const x = b * cell.w + lane(`v${b}`, p.key);\n      raw = [pOut, { x, y: pOut.y }, { x, y: pIn.y }, pIn];\n    } else {\n      const b1 = sc + 1;\n      const x1 = b1 * cell.w + lane(`v${b1}`, p.key);\n      const x2 = tc * cell.w + lane(`v${tc}`, p.key);\n      const gy = p.gy + lane(`g${p.gy}`, p.key);\n      raw = [pOut, { x: x1, y: pOut.y }, { x: x1, y: gy }, { x: x2, y: gy }, { x: x2, y: pIn.y }, pIn];\n    }\n\n    const snapped = raw.map((q) => (vertical ? { x: snap(q.y), y: snap(q.x) } : { x: snap(q.x), y: snap(q.y) }));\n    const { d, length } = orthoPath(snapped, radius);\n    return { key: p.key, from: e.from, to: e.to, d, length };\n  });\n\n  const pins: RoutePin[] = [];\n  for (const e of valid) {\n    const out = pinY.get(`out|${edgeKey(e)}`);\n    const inn = pinY.get(`in|${edgeKey(e)}`);\n    if (out !== undefined) pins.push({ id: e.from, side: \"out\", y: out });\n    if (inn !== undefined) pins.push({ id: e.to, side: \"in\", y: inn });\n  }\n\n  return { routes, pins };\n}\n\n// Pure and exported for tests. Returns one orthogonal path per edge, in grid coordinates.\nexport function routeEdges(\n  nodes: RouteNode[],\n  edges: RouteEdge[],\n  cell: { w: number; h: number } = DEFAULT_CELL,\n  r = 6,\n): { key: string; d: string }[] {\n  const resolved = resolveNodes(nodes, cell);\n  return buildRoutes(resolved, edges, cell, r).routes.map(({ key, d }) => ({ key, d }));\n}\n\ninterface Bounds {\n  x0: number;\n  y0: number;\n  w: number;\n  h: number;\n}\n\n// Resolved nodes, routes and screen bounds (grid extents plus 24px of padding) for one layout.\nfunction buildLayout(input: RouteNode[], edges: RouteEdge[], radius: number, layout: Layout) {\n  const resolved = resolveNodes(input, layout.cell, layout.vertical);\n  const built = buildRoutes(resolved, edges, layout.cell, radius, layout.halfFlow, layout.vertical);\n  let bounds: Bounds = { x0: -PAD, y0: -PAD, w: PAD * 2, h: PAD * 2 };\n  if (resolved.nodes.length > 0) {\n    const cols = resolved.nodes.map((n) => n.col);\n    const rows = resolved.nodes.map((n) => n.row);\n    const minC = Math.min(...cols);\n    const minR = Math.min(...rows);\n    const flow = { x0: minC * layout.cell.w - PAD, w: (Math.max(...cols) - minC + 1) * layout.cell.w + PAD * 2 };\n    const cross = { y0: minR * layout.cell.h - PAD, h: (Math.max(...rows) - minR + 1) * layout.cell.h + PAD * 2 };\n    bounds = layout.vertical\n      ? { x0: cross.y0, y0: flow.x0, w: cross.h, h: flow.w }\n      : { x0: flow.x0, y0: cross.y0, w: flow.w, h: cross.h };\n  }\n  return { resolved, built, bounds, vertical: layout.vertical };\n}\n\n// Nearest node in a direction on screen, scored as along + 2 × perpendicular distance.\nfunction nearestNode(resolved: Resolved, fromId: string, dir: Pt): string | null {\n  const from = resolved.screen.get(fromId);\n  if (!from) return null;\n  let best: string | null = null;\n  let bestScore = Infinity;\n  for (const n of resolved.nodes) {\n    if (n.id === fromId) continue;\n    const c = resolved.screen.get(n.id);\n    if (!c) continue;\n    const dx = c.x - from.x;\n    const dy = c.y - from.y;\n    const along = dx * dir.x + dy * dir.y;\n    if (along <= 0) continue;\n    const perp = Math.abs(dx * dir.y - dy * dir.x);\n    const score = along + 2 * perp;\n    if (score < bestScore) {\n      bestScore = score;\n      best = n.id;\n    }\n  }\n  return best;\n}\n\nexport function RouteTrace({\n  nodes,\n  edges,\n  cell,\n  cornerRadius = 6,\n  speed = 1,\n  selectedId,\n  onNodeSelect,\n  orientation = \"auto\",\n  readout = true,\n  ariaLabel = \"System diagram\",\n  tone,\n  attract = false,\n  frozen = false,\n  className,\n}: RouteTraceProps) {\n  const resolvedTone = useBjorkTone(tone);\n  const palette = BJORK_PALETTE[resolvedTone];\n  const vars = useMemo(() => chartVars(palette), [palette]);\n  const mounted = useHasMounted();\n  const prefersReducedMotion = useReducedMotion() ?? false;\n  const reduced = mounted && prefersReducedMotion;\n  const cellW = cell?.w ?? DEFAULT_CELL.w;\n  const cellH = cell?.h ?? DEFAULT_CELL.h;\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const buttonRefs = useRef(new Map<string, HTMLButtonElement>());\n  const lastInputRef = useRef(-Infinity);\n  const clockRef = useRef(0);\n  const announcer = useRef<AnnouncerHandle>(null);\n  const size = useElementSize(rootRef);\n  const describedBy = `route-trace-${useId().replace(/:/g, \"\")}`;\n\n  const [clock, setClock] = useState(0);\n  const [hoverId, setHoverId] = useState<string | null>(null);\n  const [focusId, setFocusId] = useState<string | null>(null);\n  // The node that holds the single tab stop (roving tabindex); it survives blur.\n  const [tabId, setTabId] = useState<string | null>(null);\n\n  // Both layouts are cheap; the container width picks one.\n  const horizontal = useMemo(\n    () => buildLayout(nodes, edges, cornerRadius, { vertical: false, cell: { w: cellW, h: cellH }, halfFlow: NODE_HALF_W }),\n    [nodes, edges, cornerRadius, cellW, cellH],\n  );\n  const vertical = useMemo(\n    () =>\n      buildLayout(nodes, edges, cornerRadius, {\n        vertical: true,\n        cell: { w: Math.max(NODE_HALF_H * 2 + 40, cellH), h: Math.min(cellW, VERTICAL_CROSS) },\n        halfFlow: NODE_HALF_H,\n      }),\n    [nodes, edges, cornerRadius, cellW, cellH],\n  );\n  const avail = size.width;\n  const fitH = avail > 0 ? avail / horizontal.bounds.w : 1;\n  const fitV = avail > 0 ? avail / vertical.bounds.w : 1;\n  const useVertical =\n    orientation === \"vertical\" || (orientation === \"auto\" && avail > 0 && fitH < MIN_SCALE && fitV > fitH);\n  const layout = useVertical ? vertical : horizontal;\n  const { resolved, built, bounds } = layout;\n  // Fit the width, never clip: below MIN_SCALE the graph keeps shrinking rather than scrolling.\n  // The portrait layout never grows past 1:1, so a narrow column doesn't get a towering diagram.\n  const scale = useVertical ? Math.min(1, fitV) : Math.min(MAX_SCALE, fitH);\n\n  const edgeByKey = useMemo(() => new Map(edges.map((e) => [edgeKey(e), e])), [edges]);\n\n  // Attract: one non-entry node goes down for 2.5s every 5s, and throughputs drift. Input pauses it for 4s.\n  const attractOn = attract && !reduced && !frozen;\n\n  useEffect(() => {\n    if (!attractOn) return;\n    const id = window.setInterval(() => {\n      if (rootRef.current?.dataset.loop !== \"running\") return;\n      if (performance.now() - lastInputRef.current < ATTRACT_IDLE_MS) return;\n      clockRef.current += ATTRACT_TICK_MS;\n      setClock(clockRef.current);\n    }, ATTRACT_TICK_MS);\n    return () => window.clearInterval(id);\n  }, [attractOn]);\n\n  const attractDownId = useMemo(() => {\n    if (!attractOn) return null;\n    const minCol = Math.min(...resolved.nodes.map((n) => n.col));\n    const targets = resolved.nodes\n      .filter((n) => n.col > minCol)\n      .sort((a, b) => a.col - b.col || a.row - b.row);\n    if (targets.length === 0) return null;\n    if (clock % ATTRACT_PERIOD_MS < ATTRACT_PERIOD_MS - ATTRACT_DOWN_MS) return null;\n    const slot = Math.floor(clock / ATTRACT_PERIOD_MS);\n    return targets[slot % targets.length]?.id ?? null;\n  }, [attractOn, clock, resolved]);\n\n  // Pauses the packet animation offscreen and in background tabs. With nothing to animate\n  // (reduced motion, frozen, or no speed) the loop reads idle, like the other charts.\n  const still = reduced || frozen || !(speed > 0);\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    if (still || typeof IntersectionObserver === \"undefined\") {\n      el.dataset.loop = \"idle\";\n      return;\n    }\n    let inView = true;\n    const sync = () => {\n      el.dataset.loop = inView && document.visibilityState === \"visible\" ? \"running\" : \"paused\";\n    };\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry?.isIntersecting ?? false;\n        sync();\n      },\n      { rootMargin: \"96px\" },\n    );\n    observer.observe(el);\n    document.addEventListener(\"visibilitychange\", sync);\n    sync();\n    return () => {\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", sync);\n    };\n  }, [still]);\n\n  const nodeStatus = new Map<string, RouteStatus>();\n  for (const n of resolved.nodes) {\n    nodeStatus.set(n.id, n.id === attractDownId ? \"down\" : (n.status ?? \"ok\"));\n  }\n  const edgeStatus = (from: string, to: string, own?: RouteStatus): RouteStatus => {\n    const a = nodeStatus.get(from);\n    const b = nodeStatus.get(to);\n    if (a === \"down\" || b === \"down\" || own === \"down\") return \"down\";\n    if (a === \"degraded\" || b === \"degraded\" || own === \"degraded\") return \"degraded\";\n    return \"ok\";\n  };\n\n  // Flow order (column, then row) for Home and End and the default tab stop.\n  const flowOrder = useMemo(() => [...resolved.nodes].sort((a, b) => a.col - b.col || a.row - b.row).map((n) => n.id), [resolved]);\n  const rovingId =\n    tabId && resolved.byId.has(tabId) ? tabId : selectedId && resolved.byId.has(selectedId) ? selectedId : (flowOrder[0] ?? null);\n\n  // Status summary, read by screen readers with the group and shown in the readout.\n  const byStatus: Record<RouteStatus, string[]> = { ok: [], degraded: [], down: [] };\n  for (const n of resolved.nodes) byStatus[nodeStatus.get(n.id) ?? \"ok\"].push(n.label);\n  const summaryParts = [`${byStatus.ok.length} healthy`];\n  if (byStatus.degraded.length) summaryParts.push(`${byStatus.degraded.length} degraded (${byStatus.degraded.join(\", \")})`);\n  if (byStatus.down.length) summaryParts.push(`${byStatus.down.length} down (${byStatus.down.join(\", \")})`);\n  const summary = `${resolved.nodes.length} services: ${summaryParts.join(\", \")}. ${built.routes.length} connections.`;\n\n  // Announce status changes the caller makes (not the attract loop's demo outages).\n  const statusKey = JSON.stringify(resolved.nodes.map((n) => [n.id, nodeStatus.get(n.id) ?? \"ok\"]));\n  const prevStatus = useRef<string | null>(null);\n  useEffect(() => {\n    const prev = prevStatus.current;\n    prevStatus.current = statusKey;\n    if (prev === null || prev === statusKey || attractOn) return;\n    const before = new Map(JSON.parse(prev) as [string, RouteStatus][]);\n    const changed = (JSON.parse(statusKey) as [string, RouteStatus][])\n      .filter(([id, st]) => before.has(id) && before.get(id) !== st)\n      .map(([id, st]) => `${resolved.byId.get(id)?.label ?? id} ${STATUS_WORD[st]}`);\n    if (changed.length) announcer.current?.say(changed.join(\", \"));\n  }, [statusKey, attractOn, resolved]);\n\n  const activeId = hoverId ?? focusId;\n  const transitionMs = (ms: number) => (reduced ? 0 : ms);\n  const { x0, y0, w: vbW, h: vbH } = bounds;\n  const pct = (v: number) => `${Math.round(clamp01(v) * 100)}%`;\n\n  // Incoming and outgoing traffic of one node, for its accessible name and the readout.\n  const trafficOf = (id: string) => {\n    const label = (other: string) => resolved.byId.get(other)?.label ?? other;\n    const valid = edges.filter((e) => resolved.byId.has(e.from) && resolved.byId.has(e.to) && e.from !== e.to);\n    return {\n      out: valid.filter((e) => e.from === id).map((e) => ({ id: e.to, label: label(e.to), v: e.throughput, st: edgeStatus(e.from, e.to, e.status) })),\n      in: valid.filter((e) => e.to === id).map((e) => ({ id: e.from, label: label(e.from), v: e.throughput, st: edgeStatus(e.from, e.to, e.status) })),\n    };\n  };\n\n  const onNodeKey = (event: KeyboardEvent<HTMLButtonElement>, id: string) => {\n    stampInput(lastInputRef);\n    let next: string | null = null;\n    if (event.key === \"Home\") next = flowOrder[0] ?? null;\n    else if (event.key === \"End\") next = flowOrder[flowOrder.length - 1] ?? null;\n    else if (event.key === \"Escape\") {\n      setHoverId(null);\n      return;\n    } else {\n      const dir = ARROW_DIRS[event.key];\n      if (!dir) return;\n      next = nearestNode(resolved, id, dir);\n    }\n    event.preventDefault();\n    if (next) {\n      setTabId(next);\n      buttonRefs.current.get(next)?.focus();\n    }\n  };\n\n  let readoutText = summary;\n  if (activeId && resolved.byId.has(activeId)) {\n    const n = resolved.byId.get(activeId)!;\n    const t = trafficOf(activeId);\n    const bits = [\n      ...t.in.map((e) => `from ${e.label} ${e.st === \"down\" ? \"no traffic\" : pct(e.v)}`),\n      ...t.out.map((e) => `to ${e.label} ${e.st === \"down\" ? \"no traffic\" : pct(e.v)}`),\n    ];\n    readoutText = `${n.label} · ${STATUS_WORD[nodeStatus.get(activeId) ?? \"ok\"]}${bits.length ? ` · ${bits.join(\" · \")}` : \"\"}`;\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      data-route-trace=\"\"\n      data-loop=\"idle\"\n      data-tone={resolvedTone}\n      data-orientation={useVertical ? \"vertical\" : \"horizontal\"}\n      role=\"group\"\n      aria-roledescription=\"system diagram\"\n      aria-label={`${ariaLabel}. Arrow keys move between services, Home and End jump to the first and last, Enter selects.`}\n      aria-describedby={describedBy}\n      onPointerMove={() => stampInput(lastInputRef)}\n      onPointerDown={() => stampInput(lastInputRef)}\n      onKeyDown={() => stampInput(lastInputRef)}\n      style={vars}\n      className={cn(\"relative w-full min-w-0 overflow-hidden p-2\", className)}\n    >\n      <style>{ROUTE_CSS}</style>\n      {/* Hidden until measured, so the first paint is never the wrong layout or scale. */}\n      <div className=\"mx-auto\" style={{ width: vbW * scale, height: vbH * scale, visibility: avail > 0 ? undefined : \"hidden\" }}>\n        <div\n          className=\"relative\"\n          style={{ width: vbW, height: vbH, transform: `scale(${scale})`, transformOrigin: \"0 0\" }}\n        >\n          <svg\n            aria-hidden=\"true\"\n            focusable=\"false\"\n            width={vbW}\n            height={vbH}\n            viewBox={`${x0} ${y0} ${vbW} ${vbH}`}\n            className=\"pointer-events-none absolute left-0 top-0 overflow-visible\"\n          >\n            {built.routes.map((r) => {\n              const edge = edgeByKey.get(r.key);\n              if (!edge) return null;\n              const st = edgeStatus(r.from, r.to, edge.status);\n              const drift = attractOn ? ATTRACT_DRIFT * Math.sin(clock / 1600 + (hashString(r.key) % 628) / 100) : 0;\n              const throughput = clamp01(edge.throughput + drift);\n              const touching = activeId !== null && (r.from === activeId || r.to === activeId);\n              const opacity = st === \"down\" ? DOWN_EDGE : activeId !== null && !touching ? DIM_EDGE : 1;\n              const dash = 6 + 12 * throughput;\n              // Spec count is ceil(throughput x 3), capped at 4. Short edges get fewer packets so each keeps a visible gap.\n              const specCount = Math.min(MAX_PACKETS, Math.ceil(throughput * 3));\n              const fitCount = Math.floor(r.length / (dash + MIN_PACKET_GAP));\n              const packetCount = Math.max(0, Math.min(specCount, fitCount >= 1 ? fitCount : 1, specCount));\n              const gap = Math.max(0.01, r.length / Math.max(1, packetCount) - dash);\n              const animate = !reduced && packetCount > 0 && r.length > 0 && speed > 0;\n              const duration = animate ? r.length / (PACKET_SPEED * speed) : 0;\n              const delay = animate ? ((hashString(r.key) % 1000) / 1000) * duration : 0;\n              const packetColour = st === \"degraded\" ? palette.warning : palette.accent;\n\n              return (\n                <g\n                  key={r.key}\n                  data-edge={r.key}\n                  data-status={st}\n                  style={{\n                    opacity,\n                    transition: `opacity ${transitionMs(st === \"down\" ? 240 : 160)}ms ease`,\n                  }}\n                >\n                  <path\n                    d={r.d}\n                    fill=\"none\"\n                    stroke={TRACE_COLOUR[resolvedTone].glow}\n                    strokeWidth={5}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  />\n                  <path\n                    d={r.d}\n                    fill=\"none\"\n                    stroke={TRACE_COLOUR[resolvedTone].trace}\n                    strokeWidth={1.5}\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  />\n                  {packetCount > 0 && (\n                    <path\n                      d={r.d}\n                      fill=\"none\"\n                      className=\"bjork-route-packet\"\n                      stroke={packetColour}\n                      strokeWidth={2}\n                      strokeDasharray={`${fmt(dash)} ${fmt(gap)}`}\n                      style={\n                        {\n                          opacity: st === \"down\" ? 0 : 1,\n                          transition: `opacity ${transitionMs(240)}ms ease`,\n                          animationName: animate ? \"bjork-route-packet\" : undefined,\n                          animationDuration: animate ? `${fmt(duration)}s` : undefined,\n                          animationTimingFunction: animate ? \"linear\" : undefined,\n                          animationIterationCount: animate ? \"infinite\" : undefined,\n                          animationDelay: animate ? `-${fmt(delay)}s` : undefined,\n                          animationPlayState: frozen || st === \"down\" ? \"paused\" : \"running\",\n                          // A length, not a bare number: Chrome steps a unitless calc() offset instead of tweening it.\n                          \"--rt-len\": `${fmt(r.length)}px`,\n                        } as CSSProperties\n                      }\n                    />\n                  )}\n                </g>\n              );\n            })}\n          </svg>\n\n          {resolved.nodes.map((n) => {\n            const st = nodeStatus.get(n.id) ?? \"ok\";\n            const c = resolved.screen.get(n.id) ?? { x: 0, y: 0 };\n            const left = c.x - NODE_HALF_W - x0;\n            const top = c.y - NODE_HALF_H - y0;\n            const dimmed = activeId !== null && activeId !== n.id;\n            const ledColour =\n              st === \"ok\" ? palette.success : st === \"degraded\" ? palette.warning : palette.error;\n            // LED centre on the label's cap-height centre (blur test), 1px border included\n            const ledTop = n.sublabel ? 10 : 16;\n            const selected = selectedId === n.id;\n            const t = trafficOf(n.id);\n            const links = [\n              t.out.length ? `sends to ${t.out.map((e) => `${e.label} ${pct(e.v)}`).join(\", \")}` : \"\",\n              t.in.length ? `receives from ${t.in.map((e) => e.label).join(\", \")}` : \"\",\n            ].filter(Boolean);\n\n            return (\n              <button\n                key={n.id}\n                ref={(el) => {\n                  if (el) buttonRefs.current.set(n.id, el);\n                  else buttonRefs.current.delete(n.id);\n                }}\n                type=\"button\"\n                tabIndex={n.id === rovingId ? 0 : -1}\n                aria-label={`${n.label}${n.sublabel ? `, ${n.sublabel}` : \"\"}, ${STATUS_WORD[st]}${links.length ? `. ${links.join(\"; \")}` : \"\"}`}\n                aria-current={selected ? \"true\" : undefined}\n                onClick={() => {\n                  setTabId(n.id);\n                  onNodeSelect?.(n.id);\n                }}\n                onKeyDown={(event) => onNodeKey(event, n.id)}\n                onFocus={() => {\n                  setFocusId(n.id);\n                  setTabId(n.id);\n                }}\n                onBlur={() => setFocusId((prev) => (prev === n.id ? null : prev))}\n                onPointerEnter={() => setHoverId(n.id)}\n                onPointerLeave={() => setHoverId((prev) => (prev === n.id ? null : prev))}\n                className={cn(\"absolute rounded-[8px] border text-left active:scale-[0.97]\", chartFocusRing)}\n                style={{\n                  left,\n                  top,\n                  width: NODE_HALF_W * 2,\n                  height: NODE_HALF_H * 2,\n                  borderColor: palette.borderStrong,\n                  borderWidth: 1,\n                  background: palette.surface,\n                  opacity: dimmed ? DIM_NODE : 1,\n                  transition: `opacity ${transitionMs(160)}ms ease, transform ${transitionMs(140)}ms ease-out`,\n                  outline: selected ? `1px solid ${palette.accentInk}` : undefined,\n                  outlineOffset: selected ? 3 : undefined,\n                }}\n              >\n                {built.pins\n                  .filter((p) => p.id === n.id)\n                  .map((p) => (\n                    <span\n                      key={`${p.side}-${p.y}`}\n                      aria-hidden=\"true\"\n                      className=\"pointer-events-none absolute size-1\"\n                      style={\n                        layout.vertical\n                          ? {\n                              // Pins on the top (in) and bottom (out) edges; children sit in the padding box.\n                              left: p.y - (c.x - NODE_HALF_W) - 3,\n                              top: p.side === \"in\" ? -2.5 : undefined,\n                              bottom: p.side === \"out\" ? -3.5 : undefined,\n                              background: palette.borderStrong,\n                            }\n                          : {\n                              // -2 half pin, -1 border: children are placed in the padding box\n                              top: p.y - (c.y - NODE_HALF_H) - 3,\n                              left: p.side === \"in\" ? -2.5 : undefined,\n                              right: p.side === \"out\" ? -3.5 : undefined,\n                              background: palette.borderStrong,\n                            }\n                      }\n                    />\n                  ))}\n                <span\n                  aria-hidden=\"true\"\n                  className=\"pointer-events-none absolute size-[6px] rounded-full\"\n                  style={{\n                    left: 10,\n                    top: ledTop,\n                    background: ledColour,\n                    transition: `background-color ${transitionMs(240)}ms ease`,\n                  }}\n                />\n                <span className=\"pointer-events-none absolute inset-y-0 left-[22px] right-[8px] flex flex-col justify-center\">\n                  <span className=\"block truncate font-bjork-alpha text-[12px] font-medium leading-[14px]\" style={{ color: palette.text }}>\n                    {n.label}\n                  </span>\n                  {n.sublabel && (\n                    <span className=\"block truncate font-mono text-[11px] leading-[12px] tabular-nums\" style={{ color: palette.textSoft }}>\n                      {n.sublabel}\n                    </span>\n                  )}\n                </span>\n                {st !== \"ok\" && (\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"pointer-events-none absolute right-0 -top-[14px] font-mono text-[10px] leading-[10px] [text-box:trim-both_cap_alphabetic]\"\n                    style={{ color: st === \"down\" ? palette.error : palette.warning }}\n                  >\n                    {st === \"down\" ? \"DOWN\" : \"DEGRADED\"}\n                  </span>\n                )}\n              </button>\n            );\n          })}\n        </div>\n      </div>\n\n      {readout && (\n        <p\n          aria-hidden=\"true\"\n          className=\"mx-auto mt-2 line-clamp-2 min-h-[30px] max-w-[64ch] text-balance text-center font-mono text-[11px] leading-[15px] tabular-nums text-[color:var(--bjork-text-muted)]\"\n        >\n          {readoutText}\n        </p>\n      )}\n\n      <div id={describedBy} className=\"sr-only\">\n        <p>{summary}</p>\n        <ul>\n          {built.routes.map((r) => {\n            const edge = edgeByKey.get(r.key);\n            const from = resolved.byId.get(r.from);\n            const to = resolved.byId.get(r.to);\n            if (!edge || !from || !to) return null;\n            const st = edgeStatus(r.from, r.to, edge.status);\n            return <li key={r.key}>{`${from.label} to ${to.label}, throughput ${pct(edge.throughput)}, ${STATUS_WORD[st]}`}</li>;\n          })}\n        </ul>\n      </div>\n      <ChartAnnouncer ref={announcer} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/charts/route-trace.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    },
    {
      "path": "components/bjork-ui/_core/palette.ts",
      "content": "export type BjorkTone = \"dark\" | \"light\";\n\n// Canvas, WebGL and SVG attribute colours. DOM code uses the CSS tokens with these as fallbacks.\nexport const BJORK_PALETTE = {\n  dark: {\n    bg: \"#050505\", stage: \"#111111\", surface: \"#121212\", raised: \"#161616\", active: \"#202020\",\n    border: \"#232323\", borderStrong: \"#343434\",\n    text: \"#ededed\", textMedium: \"rgba(237,237,237,0.72)\", textMuted: \"rgba(237,237,237,0.6)\",\n    textSoft: \"rgba(237,237,237,0.5)\", textFaint: \"rgba(237,237,237,0.22)\", hair: \"rgba(237,237,237,0.12)\",\n    accent: \"#ec5c13\", accentInk: \"#ec5c13\", accentFill: \"#b84a12\", accentSoft: \"rgba(236,92,19,0.12)\", accentMuted: \"rgba(236,92,19,0.24)\",\n    accentFg: \"#fff2ea\", success: \"#4cc38a\", warning: \"#f2b544\", error: \"#ff5c4d\",\n  },\n  light: {\n    bg: \"#f7f5ef\", stage: \"#f7f5ef\", surface: \"#fffcf6\", raised: \"#f5efe3\", active: \"#efe7d8\",\n    border: \"#eee6db\", borderStrong: \"#e1d7c8\",\n    text: \"#171717\", textMedium: \"rgba(23,23,23,0.78)\", textMuted: \"rgba(23,23,23,0.68)\",\n    textSoft: \"rgba(23,23,23,0.62)\", textFaint: \"rgba(23,23,23,0.22)\", hair: \"rgba(23,23,23,0.10)\",\n    accent: \"#ec7d43\", accentInk: \"#b4531f\", accentFill: \"#ec7d43\", accentSoft: \"rgba(236,125,67,0.10)\", accentMuted: \"rgba(236,125,67,0.20)\",\n    accentFg: \"#3f2112\", success: \"#1f8a55\", warning: \"#a86b00\", error: \"#c8361f\",\n  },\n} as const;\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/palette.ts"
    },
    {
      "path": "components/bjork-ui/_core/tone.ts",
      "content": "\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport type { BjorkTone } from \"./palette\";\n\nconst subscribeNoop = () => () => {};\nconst getMounted = () => true;\nconst getServerMounted = () => false;\n\n/**\n * Resolves the tone for a component. An explicit `tone` always wins.\n * Otherwise it follows next-themes' resolved theme, but only after mount,\n * so the server render and first client render agree on dark.\n */\nexport function useBjorkTone(tone?: BjorkTone): BjorkTone {\n  const { resolvedTheme } = useTheme();\n  const mounted = useSyncExternalStore(subscribeNoop, getMounted, getServerMounted);\n  if (tone) return tone;\n  if (!mounted) return \"dark\";\n  return resolvedTheme === \"light\" ? \"light\" : \"dark\";\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/tone.ts"
    },
    {
      "path": "components/bjork-ui/_core/canvas.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState, type RefObject } from \"react\";\nimport { isCoarsePointer } from \"./loop\";\n\n// Caps DPR at 1.5 on coarse pointers and 2 otherwise.\nexport function defaultMaxDpr(): number {\n  return isCoarsePointer() ? 1.5 : 2;\n}\n\n// Sets the backing size for a CSS box and returns the DPR used.\nexport function sizeCanvas(\n  canvas: HTMLCanvasElement,\n  cssW: number,\n  cssH: number,\n  maxDpr: number = defaultMaxDpr(),\n): number {\n  const dpr = Math.min(typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1, maxDpr);\n  canvas.width = Math.max(1, Math.round(cssW * dpr));\n  canvas.height = Math.max(1, Math.round(cssH * dpr));\n  canvas.style.width = `${cssW}px`;\n  canvas.style.height = `${cssH}px`;\n  return dpr;\n}\n\n// Rounded size of an element, kept current with a ResizeObserver.\nexport function useElementSize(ref: RefObject<HTMLElement | null>): { width: number; height: number } {\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver((entries) => {\n      const box = entries[0]?.contentRect;\n      if (!box) return;\n      const width = Math.round(box.width);\n      const height = Math.round(box.height);\n      setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n\n  return size;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/canvas.ts"
    },
    {
      "path": "components/bjork-ui/_core/loop.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, type RefObject } from \"react\";\n\nexport interface VisibleLoopOptions {\n  enabled?: boolean;\n  fpsCap?: number;\n  rootMargin?: string;\n}\n\n/**\n * Runs `frame(dt, t)` on requestAnimationFrame while the element is near the viewport,\n * the tab is visible and `enabled` is not false. Return `false` from `frame` to go idle\n * until `wake()` is called. Writes `data-loop` on the element: running, idle or paused.\n */\nexport function useVisibleLoop(\n  ref: RefObject<HTMLElement | null>,\n  frame: (dt: number, t: number) => boolean | void,\n  opts?: VisibleLoopOptions,\n): { wake: () => void } {\n  const enabled = opts?.enabled !== false;\n  const fpsCap = opts?.fpsCap ?? 0;\n  const rootMargin = opts?.rootMargin ?? \"96px\";\n\n  const frameRef = useRef(frame);\n  useEffect(() => {\n    frameRef.current = frame;\n  }, [frame]);\n\n  const wakeRef = useRef<() => void>(() => {});\n  const wake = useCallback(() => wakeRef.current(), []);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof window === \"undefined\") return;\n\n    let wants = true; // false once the frame callback returns false\n    let inView = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    const minFrame = fpsCap > 0 ? 1000 / fpsCap : 0;\n\n    const canRun = () => enabled && inView && document.visibilityState === \"visible\";\n    const stop = () => {\n      if (raf) cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    const update = () => {\n      if (!enabled) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      if (!wants) {\n        stop();\n        el.dataset.loop = \"idle\";\n        return;\n      }\n      if (!canRun()) {\n        stop();\n        el.dataset.loop = \"paused\";\n        return;\n      }\n      el.dataset.loop = \"running\";\n      if (!raf) {\n        last = performance.now();\n        raf = requestAnimationFrame(tick);\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (!canRun() || !wants) {\n        update();\n        return;\n      }\n      const elapsed = now - last;\n      if (minFrame && elapsed < minFrame - 0.5) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      last = now;\n      const dt = Math.min(0.05, Math.max(0, elapsed / 1000));\n      t += dt;\n      const keep = frameRef.current(dt, t);\n      if (keep === false) {\n        wants = false;\n        update();\n        return;\n      }\n      raf = requestAnimationFrame(tick);\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        inView = entry.isIntersecting;\n        update();\n      },\n      { rootMargin },\n    );\n    observer.observe(el);\n\n    const onVisibility = () => update();\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    wakeRef.current = () => {\n      wants = true;\n      update();\n    };\n\n    update();\n\n    return () => {\n      stop();\n      observer.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      wakeRef.current = () => {};\n    };\n  }, [ref, enabled, fpsCap, rootMargin]);\n\n  return useMemo(() => ({ wake }), [wake]);\n}\n\n// matchMedia based check. False on the server.\nexport function isCoarsePointer(): boolean {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n  return window.matchMedia(\"(pointer: coarse)\").matches;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/loop.ts"
    },
    {
      "path": "components/bjork-ui/_core/random.ts",
      "content": "// Seeded randomness and noise. Deterministic for a given seed, so previews and tests repeat.\n\n// Small fast PRNG. Returns floats in [0, 1).\nexport function mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n// FNV-1a 32-bit.\nexport function hashString(s: string): number {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < s.length; i++) {\n    h ^= s.charCodeAt(i);\n    h = Math.imul(h, 0x01000193);\n  }\n  return h >>> 0;\n}\n\n// Integer lattice hash to [-1, 1].\nfunction latticeHash(seed: number, x: number, y = 0): number {\n  let h = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(seed | 0, 2246822519);\n  h = Math.imul(h ^ (h >>> 13), 1274126177);\n  h ^= h >>> 16;\n  return ((h >>> 0) / 4294967295) * 2 - 1;\n}\n\nfunction smooth(t: number): number {\n  return t * t * (3 - 2 * t);\n}\n\nexport function valueNoise1D(seed: number): (x: number) => number {\n  return (x: number) => {\n    const i = Math.floor(x);\n    const f = x - i;\n    const a = latticeHash(seed, i);\n    const b = latticeHash(seed, i + 1);\n    return a + (b - a) * smooth(f);\n  };\n}\n\nexport function fbm1D(seed: number, octaves = 3): (x: number) => number {\n  const layers = Array.from({ length: octaves }, (_, o) => valueNoise1D(seed + o * 101));\n  let norm = 0;\n  for (let o = 0; o < octaves; o++) norm += 0.5 ** o;\n  return (x: number) => {\n    let sum = 0;\n    let amp = 1;\n    let freq = 1;\n    for (let o = 0; o < octaves; o++) {\n      sum += amp * layers[o](x * freq);\n      amp *= 0.5;\n      freq *= 2;\n    }\n    return sum / norm;\n  };\n}\n\nexport function valueNoise2D(seed: number): (x: number, y: number) => number {\n  return (x: number, y: number) => {\n    const xi = Math.floor(x);\n    const yi = Math.floor(y);\n    const fx = smooth(x - xi);\n    const fy = smooth(y - yi);\n    const a = latticeHash(seed, xi, yi);\n    const b = latticeHash(seed, xi + 1, yi);\n    const c = latticeHash(seed, xi, yi + 1);\n    const d = latticeHash(seed, xi + 1, yi + 1);\n    const top = a + (b - a) * fx;\n    const bottom = c + (d - c) * fx;\n    return top + (bottom - top) * fy;\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/random.ts"
    },
    {
      "path": "components/bjork-ui/charts/_kit/chrome.tsx",
      "content": "\"use client\";\n\n// Shared chart chrome for the Charts collection: theme vars, the hover tooltip, legend keys and the\n// screen-reader table twin every chart ships with.\n\nimport { forwardRef, memo, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { BJORK_PALETTE, type BjorkTone } from \"@/components/bjork-ui/_core/palette\";\nimport { useBjorkTone } from \"@/components/bjork-ui/_core/tone\";\nimport { LiveRegion } from \"@/components/bjork-ui/_core/a11y\";\nimport { surfaceVars } from \"@/components/bjork-ui/_core/surface\";\n\nexport type ChartPalette = (typeof BJORK_PALETTE)[BjorkTone];\n\n// Tokens a chart root writes so DOM overlays follow `tone` even outside a themed page.\nexport function chartVars(pal: ChartPalette): CSSProperties {\n  return {\n    ...surfaceVars(pal === BJORK_PALETTE.light ? \"light\" : \"dark\"),\n    \"--bjork-accent\": pal.accent,\n    \"--bjork-accent-ink\": pal.accentInk,\n    \"--bjork-accent-fill\": pal.accentFill,\n    \"--bjork-accent-soft\": pal.accentSoft,\n    \"--bjork-accent-foreground\": pal.accentFg,\n    \"--bjork-text\": pal.text,\n    \"--bjork-text-medium\": pal.textMedium,\n    \"--bjork-text-muted\": pal.textMuted,\n    \"--bjork-text-soft\": pal.textSoft,\n    \"--bjork-text-faint\": pal.textFaint,\n    \"--bjork-hair\": pal.hair,\n    \"--bjork-border\": pal.border,\n    \"--bjork-border-strong\": pal.borderStrong,\n    \"--bjork-surface\": pal.surface,\n    \"--bjork-surface-hover\": pal.raised,\n    \"--bjork-ring-offset\": pal.bg,\n    // The surface a chart sits on, for label halos and cut-outs. Override it on the root if needed.\n    \"--bjork-chart-bg\": pal.stage,\n    \"--bjork-success\": pal.success,\n    \"--bjork-warning\": pal.warning,\n    \"--bjork-error\": pal.error,\n  } as CSSProperties;\n}\n\n// Tone, palette, reduced motion and root vars in one call.\nexport function useChartTheme(toneProp?: BjorkTone) {\n  const tone = useBjorkTone(toneProp);\n  const pal = BJORK_PALETTE[tone];\n  const reduce = !!useReducedMotion();\n  return { tone, pal, reduce, vars: chartVars(pal) };\n}\n\n// Focus ring shared by every focusable plot surface.\nexport const chartFocusRing =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--bjork-ring-offset)]\";\n\n// Mono label style: 10px Geist Mono, tabular, cap-trimmed.\nexport const chartMono = \"font-mono text-[10px] leading-none tabular-nums [text-box:trim-both_cap_alphabetic]\";\n\n// Floating chart surfaces follow the library canon (_core/surface.ts): a 1px border plus the inset light\n// shadow. Hover cards take the full surface shadow; small readout tags take the soft one. Colours come\n// from chartVars, so light and dark both follow the tone.\nexport const chartCardEdge = \"border border-[color:var(--bjork-border)] shadow-[var(--bjork-shadow-surface)]\";\nexport const chartTagEdge = \"border border-[color:var(--bjork-border)] shadow-[var(--bjork-shadow-soft)]\";\n\nexport interface TooltipRow {\n  key?: string;\n  label: ReactNode;\n  value: ReactNode;\n  color?: string;\n  dashed?: boolean;\n  strong?: boolean;\n}\n\n// The hover card. Values lead, labels follow, series keyed with a short line in the series colour.\n// Position it with `style.transform` from the caller; it never takes pointer events.\nexport const ChartTooltip = forwardRef<\n  HTMLDivElement,\n  { title?: ReactNode; rows: TooltipRow[]; visible: boolean; className?: string; style?: CSSProperties; tipKey?: string }\n>(function ChartTooltip({ title, rows, visible, className, style, tipKey }, ref) {\n  return (\n    <div\n      ref={ref}\n      data-key={tipKey}\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(\n        `pointer-events-none absolute left-0 top-0 z-10 min-w-[120px] rounded-[10px] ${chartCardEdge} bg-[color:var(--bjork-surface)] px-2.5 py-2 transition-opacity duration-150 ease-out`,\n        visible ? \"opacity-100\" : \"opacity-0\",\n        className,\n      )}\n      style={style}\n    >\n      {title !== undefined && (\n        <div className=\"mb-1.5 whitespace-nowrap font-mono text-[10px] uppercase leading-none tracking-[0.08em] text-[color:var(--bjork-text-soft)] [text-box:trim-both_cap_alphabetic]\">\n          {title}\n        </div>\n      )}\n      <div className=\"flex flex-col gap-[7px]\">\n        {rows.map((r, i) => (\n          <div key={r.key ?? i} className=\"flex items-center gap-2 whitespace-nowrap leading-none\">\n            {r.color && (\n              <span\n                aria-hidden=\"true\"\n                className=\"inline-block w-2.5 shrink-0\"\n                style={\n                  r.dashed\n                    ? { borderTop: `1.5px dashed ${r.color}`, height: 0 }\n                    : { background: r.color, height: 2, borderRadius: 1 }\n                }\n              />\n            )}\n            <span\n              className={cn(\n                \"font-mono text-[12px] tabular-nums [text-box:trim-both_cap_alphabetic]\",\n                r.strong === false ? \"text-[color:var(--bjork-text-medium)]\" : \"text-[color:var(--bjork-text)]\",\n              )}\n            >\n              {r.value}\n            </span>\n            <span className=\"ml-auto pl-3 font-bjork-alpha text-[11px] text-[color:var(--bjork-text-muted)] [text-box:trim-both_cap_alphabetic]\">\n              {r.label}\n            </span>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n});\n\n// Places a tooltip of size (tw, th) next to (x, y) inside a (w, h) box, flipping away from edges.\nexport function placeTooltip(\n  x: number,\n  y: number,\n  tw: number,\n  th: number,\n  w: number,\n  h: number,\n  gap = 12,\n): { x: number; y: number } {\n  let tx = x + gap;\n  if (tx + tw > w) tx = x - gap - tw;\n  if (tx < 0) tx = Math.max(0, Math.min(w - tw, x - tw / 2));\n  let ty = y - th - gap;\n  if (ty < 0) ty = Math.min(h - th, y + gap);\n  return { x: Math.round(tx), y: Math.round(Math.max(0, ty)) };\n}\n\nexport interface LegendItem {\n  id: string;\n  label: string;\n  color: string;\n  shape?: \"line\" | \"dash\" | \"rect\" | \"dot\";\n  muted?: boolean;\n}\n\n// A legend row. Keys mirror the mark: line for lines, rect for areas and bars.\nexport function ChartLegend({\n  items,\n  className,\n  onHover,\n  activeId,\n}: {\n  items: LegendItem[];\n  className?: string;\n  onHover?: (id: string | null) => void;\n  activeId?: string | null;\n}) {\n  return (\n    <div className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2\", className)}>\n      {items.map((it) => {\n        const dim = (activeId && activeId !== it.id) || it.muted;\n        return (\n          <span\n            key={it.id}\n            onPointerEnter={onHover ? () => onHover(it.id) : undefined}\n            onPointerLeave={onHover ? () => onHover(null) : undefined}\n            className={cn(\n              \"inline-flex items-center gap-1.5 font-bjork-alpha text-[11px] font-medium leading-3 transition-opacity duration-150 ease-out\",\n              dim ? \"opacity-45\" : \"opacity-100\",\n              \"text-[color:var(--bjork-text-medium)]\",\n            )}\n          >\n            <LegendKey color={it.color} shape={it.shape} />\n            {it.label}\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function LegendKey({ color, shape = \"rect\" }: { color: string; shape?: LegendItem[\"shape\"] }) {\n  if (shape === \"line\") return <span aria-hidden=\"true\" className=\"inline-block h-[2px] w-2.5 rounded-[1px]\" style={{ background: color }} />;\n  if (shape === \"dash\") return <span aria-hidden=\"true\" className=\"inline-block h-0 w-2.5\" style={{ borderTop: `1.5px dashed ${color}` }} />;\n  if (shape === \"dot\") return <span aria-hidden=\"true\" className=\"inline-block size-2 rounded-full\" style={{ background: color }} />;\n  return <span aria-hidden=\"true\" className=\"inline-block size-2 rounded-[2px]\" style={{ background: color }} />;\n}\n\n// The table twin. Every value the chart shows is reachable here without hovering.\n// Memoised: hover state re-renders the chart, never the table. Pass memoised rows.\nexport const ChartTable = memo(function ChartTable({\n  caption,\n  columns,\n  rows,\n}: {\n  caption: string;\n  columns: string[];\n  rows: (string | number)[][];\n}) {\n  return (\n    <table className=\"sr-only\">\n      <caption>{caption}</caption>\n      <thead>\n        <tr>\n          {columns.map((c) => (\n            <th key={c} scope=\"col\">\n              {c}\n            </th>\n          ))}\n        </tr>\n      </thead>\n      <tbody>\n        {rows.map((r, i) => (\n          <tr key={i}>\n            {r.map((cell, j) =>\n              j === 0 ? (\n                <th key={j} scope=\"row\">\n                  {cell}\n                </th>\n              ) : (\n                <td key={j}>{cell}</td>\n              ),\n            )}\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  );\n});\n\n// A pool of absolutely positioned spans the draw loop fills through `writeLabels`.\nexport function LabelPool({\n  count,\n  pool,\n  className,\n}: {\n  count: number;\n  pool: { current: (HTMLSpanElement | null)[] };\n  className?: string;\n}) {\n  return (\n    <>\n      {Array.from({ length: count }).map((_, i) => (\n        <span\n          key={i}\n          ref={(el) => {\n            pool.current[i] = el;\n          }}\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute left-0 top-0 whitespace-nowrap opacity-0\",\n            className ?? \"font-mono text-[10px] leading-none tabular-nums text-[color:var(--bjork-text-faint)] [text-box:trim-both_cap_alphabetic]\",\n          )}\n        />\n      ))}\n    </>\n  );\n}\n\nexport interface TooltipContent {\n  key: string;\n  title?: ReactNode;\n  rows: TooltipRow[];\n}\n\nexport interface TooltipHandle {\n  /** Shows `content`, or hides with `null`. Same key is a no-op. */\n  set: (content: TooltipContent | null) => void;\n  readonly el: HTMLDivElement | null;\n  /** Measured size of the current content. */\n  readonly size: { w: number; h: number };\n}\n\n// A tooltip that owns its state, so hovering re-renders only the tooltip, never the chart.\n// The chart positions it from its draw loop through `handle.el.style.transform`.\nexport const HoverTooltip = forwardRef<TooltipHandle, { onMeasure?: () => void; className?: string }>(function HoverTooltip(\n  { onMeasure, className },\n  ref,\n) {\n  const [state, setState] = useState<{ content: TooltipContent | null; last: TooltipContent | null }>({ content: null, last: null });\n  const elRef = useRef<HTMLDivElement>(null);\n  const size = useRef({ w: 0, h: 0 });\n  const onMeasureRef = useRef(onMeasure);\n  useEffect(() => {\n    onMeasureRef.current = onMeasure;\n  });\n  useImperativeHandle(\n    ref,\n    () => ({\n      set: (content) =>\n        setState((prev) => {\n          if ((prev.content?.key ?? null) === (content?.key ?? null)) return prev;\n          return { content, last: content ?? prev.last };\n        }),\n      get el() {\n        return elRef.current;\n      },\n      get size() {\n        return size.current;\n      },\n    }),\n    [],\n  );\n  useLayoutEffect(() => {\n    const el = elRef.current;\n    if (!el) return;\n    size.current = { w: el.offsetWidth, h: el.offsetHeight };\n    onMeasureRef.current?.();\n  }, [state.last]);\n  const shown = state.content ?? state.last;\n  return <ChartTooltip ref={elRef} title={shown?.title} rows={shown?.rows ?? []} visible={!!state.content} className={className} />;\n});\n\nexport interface AnnouncerHandle {\n  say: (message: string) => void;\n}\n\n// Polite live region with a 250ms trailing throttle, isolated from the chart's renders.\nexport const ChartAnnouncer = forwardRef<AnnouncerHandle, { ms?: number }>(function ChartAnnouncer({ ms = 250 }, ref) {\n  const [message, setMessage] = useState(\"\");\n  const at = useRef(0);\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  useEffect(\n    () => () => {\n      if (timer.current) clearTimeout(timer.current);\n    },\n    [],\n  );\n  useImperativeHandle(\n    ref,\n    () => ({\n      say: (text: string) => {\n        const now = performance.now();\n        if (timer.current) clearTimeout(timer.current);\n        if (now - at.current >= ms) {\n          at.current = now;\n          setMessage(text);\n        } else {\n          timer.current = setTimeout(() => {\n            at.current = performance.now();\n            setMessage(text);\n          }, ms);\n        }\n      },\n    }),\n    [ms],\n  );\n  return <LiveRegion message={message} />;\n});\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/charts/_kit/chrome.tsx"
    },
    {
      "path": "components/bjork-ui/_core/a11y.tsx",
      "content": "import type { ReactNode } from \"react\";\n\n// Text for assistive tech only.\nexport function VisuallyHidden({ children }: { children: ReactNode }) {\n  return <span className=\"sr-only\">{children}</span>;\n}\n\n// Announces `message` when it changes. Same text re-rendered does not re-announce.\nexport function LiveRegion({\n  message,\n  politeness = \"polite\",\n}: {\n  message: string;\n  politeness?: \"polite\" | \"assertive\";\n}) {\n  return (\n    <span role=\"status\" aria-live={politeness} aria-atomic=\"true\" className=\"sr-only\">\n      {message}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/a11y.tsx"
    },
    {
      "path": "components/bjork-ui/_core/surface.ts",
      "content": "import type { CSSProperties } from \"react\";\nimport type { BjorkTone } from \"./palette\";\n\n// The library surface (CANON.md §1). Values mirror app/globals.css :root and .dark. Change both together.\nexport const BJORK_SURFACE = {\n  light: {\n    surface: \"#fffcf6\", surfaceMuted: \"rgba(250, 246, 237, 0.94)\", surfaceHover: \"#f5efe3\", surfaceActive: \"#efe7d8\",\n    panel: \"#f8f2e7\", menu: \"rgba(255, 252, 246, 0.98)\", field: \"rgba(255, 252, 246, 0.96)\",\n    fieldMuted: \"rgba(248, 242, 231, 0.82)\", fieldInset: \"rgba(239, 231, 216, 0.72)\",\n    border: \"#eee6db\", borderMuted: \"#f5ede2\", borderStrong: \"#e1d7c8\",\n    shadowSurface: \"inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)\",\n    shadowSoft: \"inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)\",\n    shadowPanel: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)\",\n    shadowMenu: \"inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)\",\n    shadowInset: \"inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)\",\n  },\n  dark: {\n    surface: \"#121212\", surfaceMuted: \"rgba(22, 22, 22, 0.92)\", surfaceHover: \"#161616\", surfaceActive: \"#202020\",\n    panel: \"#0d0d0d\", menu: \"rgba(18, 18, 18, 0.98)\", field: \"rgba(18, 18, 18, 0.95)\",\n    fieldMuted: \"rgba(18, 18, 18, 0.55)\", fieldInset: \"rgba(9, 9, 9, 0.75)\",\n    border: \"#232323\", borderMuted: \"#1c1c1c\", borderStrong: \"#343434\",\n    shadowSurface: \"inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)\",\n    shadowSoft: \"inset 0 1px 0 rgba(255, 255, 255, 0.045)\",\n    shadowPanel: \"inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)\",\n    shadowMenu: \"inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)\",\n    shadowInset: \"inset 0 1px 10px rgba(0, 0, 0, 0.45)\",\n  },\n} as const;\n\n/** Writes the surface tokens for a pinned tone as --bjork-* custom properties. Spread first; kit-specific keys may override. */\nexport function surfaceVars(tone: BjorkTone): CSSProperties {\n  const s = BJORK_SURFACE[tone];\n  return {\n    \"--bjork-surface\": s.surface, \"--bjork-surface-muted\": s.surfaceMuted, \"--bjork-surface-hover\": s.surfaceHover,\n    \"--bjork-surface-active\": s.surfaceActive, \"--bjork-panel\": s.panel, \"--bjork-menu\": s.menu, \"--bjork-field\": s.field,\n    \"--bjork-field-muted\": s.fieldMuted, \"--bjork-field-inset\": s.fieldInset, \"--bjork-border\": s.border,\n    \"--bjork-border-muted\": s.borderMuted, \"--bjork-border-strong\": s.borderStrong,\n    \"--bjork-shadow-surface\": s.shadowSurface, \"--bjork-shadow-soft\": s.shadowSoft, \"--bjork-shadow-panel\": s.shadowPanel,\n    \"--bjork-shadow-menu\": s.shadowMenu, \"--bjork-shadow-inset\": s.shadowInset,\n  } as CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/_core/surface.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.78)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.68)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.62)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-accent-fill": "#ec7d43",
      "bjork-accent-fill-hover": "#f0935f",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.6)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.5)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#b84a12",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-accent-fill": "#b84a12",
      "bjork-accent-fill-hover": "#a84a14",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "Charts",
    "registryUrl": "https://ui.isaiahbjork.com/route-trace.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/route-trace.json"
  },
  "type": "registry:component"
}