{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "live-line",
  "title": "Live line",
  "description": "Live line is a production-ready charts component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "next-themes",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/charts/live-line.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent,\n} from \"react\";\nimport { useReducedMotion } from \"framer-motion\";\nimport { Pause, Play } from \"lucide-react\";\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 { useVisibleLoop } from \"@/components/bjork-ui/_core/loop\";\nimport { sizeCanvas, useElementSize } from \"@/components/bjork-ui/_core/canvas\";\nimport { mulberry32 } from \"@/components/bjork-ui/_core/random\";\nimport { LiveRegion } from \"@/components/bjork-ui/_core/a11y\";\nimport { chartCardEdge } from \"@/components/bjork-ui/charts/_kit/chrome\";\n\nexport interface LivePoint {\n  t: number;\n  v: number;\n}\n\nexport interface LiveSeries {\n  id: string;\n  label: string;\n  data: LivePoint[];\n  color?: string;\n}\n\nexport interface LiveLineProps {\n  data?: LivePoint[];\n  series?: LiveSeries[];\n  window?: number;\n  easing?: number;\n  yDomain?: [number, number] | \"auto\";\n  threshold?: { value: number; label?: string };\n  formatValue?: (v: number) => string;\n  formatTime?: (t: number) => string;\n  scrub?: boolean;\n  onScrub?: (index: number, p: LivePoint) => void;\n  paused?: boolean;\n  onPausedChange?: (paused: boolean) => void;\n  /** Shows the pause/resume button in the top-right corner. */\n  showPauseButton?: boolean;\n  height?: number;\n  ariaLabel?: string;\n  tone?: BjorkTone;\n  attract?: boolean;\n  className?: string;\n}\n\n// Plot insets. Left holds value labels, right holds the 64px badge column plus an 8px gap.\nconst PAD_TOP = 12;\nconst PAD_BOTTOM = 24;\nconst PAD_LEFT = 40;\nconst PAD_RIGHT = 72;\nconst BADGE_H = 22;\nconst RANGE_TAU = 0.4; // seconds, first-order contraction of the y range\nconst CONTRACT_PX = 2; // max px a range edge may move per frame while contracting\nconst HALO_PERIOD = 1.2; // seconds\nconst ATTRACT_STEP = 250; // ms, 4Hz\nconst ATTRACT_IDLE_MS = 4000;\nconst ARIA_EVERY_MS = 5000;\nconst HIT_CAP = 4096;\nconst SETTLE_EPS = 1e-4; // fraction of the visible range\n\nfunction defaultFormatValue(v: number): string {\n  return new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 1 }).format(v);\n}\n\nfunction defaultFormatTime(t: number): string {\n  return new Date(t).toLocaleTimeString(\"en-GB\", { hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" });\n}\n\nfunction formatWindow(ms: number): string {\n  if (ms < 60000) return `${Math.round(ms / 1000)}s`;\n  if (ms % 60000 === 0) return `${ms / 60000}m`;\n  return `${Math.round(ms / 1000)}s`;\n}\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  const n = parseInt(hex.replace(\"#\", \"\"), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction rgba(rgb: [number, number, number], a: number): string {\n  return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`;\n}\n\n// First index whose t is >= value.\nfunction lowerBound(samples: LivePoint[], value: number): number {\n  let lo = 0;\n  let hi = samples.length;\n  while (lo < hi) {\n    const mid = (lo + hi) >>> 1;\n    if (samples[mid].t < value) lo = mid + 1;\n    else hi = mid;\n  }\n  return lo;\n}\n\n// First index whose t is > value.\nfunction upperBound(samples: LivePoint[], value: number): number {\n  let lo = 0;\n  let hi = samples.length;\n  while (lo < hi) {\n    const mid = (lo + hi) >>> 1;\n    if (samples[mid].t <= value) lo = mid + 1;\n    else hi = mid;\n  }\n  return lo;\n}\n\n// Grid values at \"nice\" steps (1, 2, 2.5 or 5 x 10^k), strictly inside lo..hi. Prefers 4 lines.\nfunction gridValues(lo: number, hi: number): number[] {\n  const range = hi - lo;\n  if (!(range > 0)) return [];\n  const base = Math.floor(Math.log10(range)) - 1;\n  let fallback: number[] = [];\n  for (let k = base; k <= base + 3; k++) {\n    for (const m of [1, 2, 2.5, 5]) {\n      const step = m * 10 ** k;\n      const first = Math.floor(lo / step) + 1;\n      const last = Math.ceil(hi / step) - 1;\n      const count = last - first + 1;\n      if (count < 2 || count > 4) continue;\n      const values: number[] = [];\n      for (let n = first; n <= last; n++) values.push(Number((n * step).toFixed(10)));\n      if (count === 4) return values;\n      if (fallback.length < 3) fallback = values;\n    }\n  }\n  return fallback;\n}\n\n// Seeded random walk for demos and the attract idle state. next(t) takes ms and returns a point.\nexport function createRandomWalk(\n  seed: number,\n  opts?: { base?: number; volatility?: number; hz?: number },\n): { next: (t: number) => LivePoint } {\n  const base = opts?.base ?? 120;\n  const volatility = opts?.volatility ?? 3;\n  const hz = opts?.hz ?? 4;\n  const rnd = mulberry32(seed);\n  let v = base;\n  let last: number | null = null;\n  const gauss = () => {\n    const u = Math.max(rnd(), 1e-9);\n    const w = rnd();\n    return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * w);\n  };\n  return {\n    next(t: number) {\n      const dt = last === null ? 1 / hz : Math.max(0, (t - last) / 1000);\n      last = t;\n      const k = dt > 0 ? dt : 1 / hz;\n      v += (base - v) * Math.min(1, 0.5 * k) + gauss() * volatility * Math.sqrt(k * hz);\n      return { t, v };\n    },\n  };\n}\n\ninterface Buffer {\n  id: string;\n  color?: string;\n  samples: LivePoint[];\n}\n\ninterface RunState {\n  clock: number | null;\n  range: { lo: number; hi: number } | null;\n  head: number | null;\n  attract: LivePoint[];\n  attractNext: number | null;\n  walk: ReturnType<typeof createRandomWalk> | null;\n  lastInput: number;\n  scrubT: number | null;\n  scrubSource: \"pointer\" | \"keyboard\" | null;\n  primary: LivePoint[];\n  plot: { l: number; r: number; t: number; b: number };\n  hitCount: number;\n  hitX: Float32Array;\n  hitIdx: Int32Array;\n  visMin: number;\n  visMax: number;\n  lastAria: number;\n  tipText: string;\n  tipW: number;\n  labelText: string[];\n  badgeText: string;\n}\n\nexport function LiveLine({\n  data,\n  series,\n  window: windowMs = 60000,\n  easing = 0.08,\n  yDomain = \"auto\",\n  threshold,\n  formatValue = defaultFormatValue,\n  formatTime = defaultFormatTime,\n  scrub = false,\n  onScrub,\n  paused: pausedProp,\n  showPauseButton = true,\n  onPausedChange,\n  height = 220,\n  ariaLabel = \"Live chart\",\n  tone: toneProp,\n  attract = false,\n  className,\n}: LiveLineProps) {\n  const tone = useBjorkTone(toneProp);\n  const pal = BJORK_PALETTE[tone];\n  const reduce = !!useReducedMotion();\n\n  const [pausedState, setPausedState] = useState(false);\n  const paused = pausedProp ?? pausedState;\n\n  const [announce, setAnnounce] = useState(\"\");\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const badgeRef = useRef<HTMLSpanElement>(null);\n  const tipRef = useRef<HTMLDivElement>(null);\n  const thresholdRef = useRef<HTMLSpanElement>(null);\n  const gridLabelRefs = useRef<(HTMLSpanElement | null)[]>([]);\n  const rectRef = useRef<DOMRect | null>(null);\n  const leaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const announceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const announceAt = useRef(0);\n\n  const size = useElementSize(rootRef);\n  const sizeRef = useRef({ w: 0, h: 0, dpr: 1 });\n\n  const bufRef = useRef<Buffer[]>([]);\n  const cfgRef = useRef({\n    windowMs,\n    easing,\n    yDomain,\n    threshold,\n    formatValue,\n    formatTime,\n    scrub,\n    onScrub,\n    ariaLabel,\n    reduce,\n    attract,\n    hasInput: false,\n  });\n  const palRef = useRef(pal);\n  const pausedRef = useRef(paused);\n  const stRef = useRef<RunState>({\n    clock: null,\n    range: null,\n    head: null,\n    attract: [],\n    attractNext: null,\n    walk: null,\n    lastInput: 0,\n    scrubT: null,\n    scrubSource: null,\n    primary: [],\n    plot: { l: 0, r: 0, t: 0, b: 0 },\n    hitCount: 0,\n    hitX: new Float32Array(HIT_CAP),\n    hitIdx: new Int32Array(HIT_CAP),\n    visMin: 0,\n    visMax: 0,\n    lastAria: -Infinity,\n    tipText: \"\",\n    tipW: 0,\n    labelText: [],\n    badgeText: \"\",\n  });\n\n  const frame = (dt: number, t: number): boolean => {\n    const canvas = canvasRef.current;\n    const root = rootRef.current;\n    const st = stRef.current;\n    const cfg = cfgRef.current;\n    const p = palRef.current;\n    const isPaused = pausedRef.current;\n    const { w, h, dpr } = sizeRef.current;\n    if (!canvas || !root || !w || !h) return true;\n\n    const useAttract = cfg.attract && !cfg.hasInput;\n    const buffers: Buffer[] = useAttract ? [{ id: \"attract\", samples: st.attract }] : bufRef.current;\n    const hasData = buffers.some((b) => b.samples.length > 0);\n\n    // Clock: the x axis runs on it. It only moves while running and jumps forward to new data.\n    let newest = -Infinity;\n    for (const b of buffers) {\n      const last = b.samples[b.samples.length - 1];\n      if (last && last.t > newest) newest = last.t;\n    }\n    if (st.clock === null) st.clock = Number.isFinite(newest) ? newest : Date.now();\n    if (!isPaused) st.clock += dt * 1000;\n    if (!isPaused && Number.isFinite(newest) && newest > st.clock) st.clock = newest;\n    const clock = st.clock;\n    const windowLen = cfg.windowMs;\n    const wStart = clock - windowLen;\n\n    // Attract: a seeded walk at 4Hz, paused by input and resumed after 4s idle. Static under reduced motion.\n    if (useAttract) {\n      if (!st.walk) st.walk = createRandomWalk(7, { base: 120, volatility: 3, hz: 4 });\n      const walk = st.walk;\n      if (st.attractNext === null) {\n        for (let tt = clock - 60000; tt <= clock; tt += ATTRACT_STEP) st.attract.push(walk.next(tt));\n        st.attractNext = clock + ATTRACT_STEP;\n      } else if (!cfg.reduce) {\n        const inputRecently = st.lastInput !== 0 && performance.now() - st.lastInput <= ATTRACT_IDLE_MS;\n        if (inputRecently || isPaused) {\n          st.attractNext = clock + ATTRACT_STEP;\n        } else {\n          while (st.attractNext <= clock) {\n            st.attract.push(walk.next(st.attractNext));\n            st.attractNext += ATTRACT_STEP;\n          }\n        }\n        while (st.attract.length && st.attract[0].t < clock - 300000) st.attract.shift();\n      }\n    }\n\n    const plot = {\n      l: PAD_LEFT,\n      r: Math.max(PAD_LEFT + 20, w - PAD_RIGHT),\n      t: PAD_TOP,\n      b: Math.max(PAD_TOP + 20, h - PAD_BOTTOM),\n    };\n    st.plot = plot;\n    const pW = plot.r - plot.l;\n    const pH = plot.b - plot.t;\n    const xOf = (tt: number) => plot.l + ((tt - wStart) / windowLen) * pW;\n\n    // Visible window: min and max, hit cache for the primary series, and the primary run list.\n    const primary = buffers[0]?.samples ?? [];\n    st.primary = primary;\n    let vMin = Infinity;\n    let vMax = -Infinity;\n    let hitCount = 0;\n    const primaryStart = Math.max(0, lowerBound(primary, wStart) - 1);\n    const primaryNewest = upperBound(primary, clock) - 1; // last sample at or before the clock\n    const newestFinite = primaryNewest >= 0 && Number.isFinite(primary[primaryNewest].v);\n    for (const b of buffers) {\n      const s = b.samples;\n      const from = Math.max(0, lowerBound(s, wStart) - 1);\n      const to = upperBound(s, clock);\n      for (let i = from; i < to; i++) {\n        const v = s[i].v;\n        if (!Number.isFinite(v)) continue;\n        if (v < vMin) vMin = v;\n        if (v > vMax) vMax = v;\n      }\n    }\n    for (let i = primaryStart; i < primary.length && primary[i].t <= clock; i++) {\n      const v = primary[i].v;\n      if (!Number.isFinite(v)) continue;\n      if (hitCount < HIT_CAP) {\n        st.hitX[hitCount] = xOf(primary[i].t);\n        st.hitIdx[hitCount] = i;\n        hitCount++;\n      }\n    }\n    st.hitCount = hitCount;\n\n    if (!Number.isFinite(vMin)) {\n      vMin = 0;\n      vMax = 100;\n    }\n    st.visMin = vMin;\n    st.visMax = vMax;\n\n    // Target domain: visible data (plus the threshold) padded by 12%. A fixed yDomain wins.\n    let tLo: number;\n    let tHi: number;\n    if (Array.isArray(cfg.yDomain)) {\n      tLo = cfg.yDomain[0];\n      tHi = cfg.yDomain[1];\n    } else {\n      let lo = vMin;\n      let hi = vMax;\n      if (cfg.threshold && Number.isFinite(cfg.threshold.value)) {\n        lo = Math.min(lo, cfg.threshold.value);\n        hi = Math.max(hi, cfg.threshold.value);\n      }\n      let span = hi - lo;\n      if (span < 1e-9) span = Math.max(Math.abs(hi) * 0.2, 1);\n      const pad = span * 0.12;\n      tLo = lo - pad;\n      tHi = hi + pad;\n    }\n\n    // Range: expands the same frame, contracts with a first-order approach capped per frame.\n    if (!st.range) st.range = { lo: tLo, hi: tHi };\n    const range = st.range;\n    if (Array.isArray(cfg.yDomain)) {\n      range.lo = tLo;\n      range.hi = tHi;\n    } else {\n      if (tLo < range.lo) range.lo = tLo;\n      if (tHi > range.hi) range.hi = tHi;\n      if (cfg.reduce) {\n        // Reduced motion: contraction is instant.\n        if (tLo > range.lo) range.lo = tLo;\n        if (tHi < range.hi) range.hi = tHi;\n      } else {\n        const pxPerUnit = pH / Math.max(1e-9, range.hi - range.lo);\n        const cap = CONTRACT_PX / pxPerUnit;\n        const k = 1 - Math.exp(-dt / RANGE_TAU);\n        if (tLo > range.lo) range.lo += Math.min(cap, (tLo - range.lo) * k);\n        if (tHi < range.hi) range.hi -= Math.min(cap, (range.hi - tHi) * k);\n        if (range.lo > tLo && range.lo - tLo < 1e-9 * (range.hi - range.lo)) range.lo = tLo;\n        if (range.hi < tHi && tHi - range.hi < 1e-9 * (range.hi - range.lo)) range.hi = tHi;\n      }\n    }\n    const rLo = range.lo;\n    const rHi = range.hi;\n    const yOf = (v: number) => plot.t + ((rHi - v) / Math.max(1e-9, rHi - rLo)) * pH;\n    const rangeSettled =\n      Array.isArray(cfg.yDomain) ||\n      (Math.abs(range.lo - tLo) < SETTLE_EPS * (rHi - rLo) && Math.abs(range.hi - tHi) < SETTLE_EPS * (rHi - rLo));\n\n    // Head: the newest real value, approached with a damped step. The newest sample is drawn as the head.\n    const newestVal = newestFinite ? primary[primaryNewest].v : null;\n    if (newestVal !== null) {\n      if (st.head === null || cfg.reduce) st.head = newestVal;\n      else {\n        const alpha = 1 - Math.pow(1 - cfg.easing, dt * 60);\n        st.head += (newestVal - st.head) * alpha;\n      }\n    }\n    const headSettled = newestVal === null || st.head === null || Math.abs(st.head - newestVal) < SETTLE_EPS * (rHi - rLo);\n    const headY = st.head !== null && newestVal !== null ? yOf(st.head) : null;\n\n    // Clear and frame.\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return true;\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    ctx.clearRect(0, 0, w, h);\n\n    const hair = p.hair;\n\n    // Grid\n    const grid = gridValues(rLo, rHi);\n    ctx.lineWidth = 1;\n    ctx.strokeStyle = hair;\n    ctx.beginPath();\n    grid.forEach((gv) => {\n      const y = Math.round(yOf(gv)) + 0.5;\n      ctx.moveTo(plot.l, y);\n      ctx.lineTo(plot.r, y);\n    });\n    ctx.stroke();\n\n    // Grid labels: pooled spans, positioned by transform only.\n    for (let i = 0; i < gridLabelRefs.current.length; i++) {\n      const el = gridLabelRefs.current[i];\n      if (!el) continue;\n      if (i < grid.length) {\n        const text = cfg.formatValue(grid[i]);\n        if (st.labelText[i] !== text) {\n          st.labelText[i] = text;\n          el.textContent = text;\n        }\n        el.style.opacity = \"1\";\n        // Right edge sits 4px inside the 40px gutter. The box is anchored by its own width.\n        el.style.transform = `translate3d(calc(36px - 100%), ${yOf(grid[i]).toFixed(2)}px, 0) translateY(-50%)`;\n      } else {\n        el.style.opacity = \"0\";\n      }\n    }\n\n    // Threshold\n    const thr = cfg.threshold;\n    let thrY: number | null = null;\n    if (thr && Number.isFinite(thr.value)) {\n      thrY = yOf(thr.value);\n      ctx.save();\n      ctx.setLineDash([3, 3]);\n      ctx.strokeStyle = p.textSoft;\n      ctx.beginPath();\n      const y = Math.round(thrY) + 0.5;\n      ctx.moveTo(plot.l, y);\n      ctx.lineTo(plot.r, y);\n      ctx.stroke();\n      ctx.restore();\n    }\n    if (thresholdRef.current) {\n      const el = thresholdRef.current;\n      if (thr && thrY !== null) {\n        el.style.opacity = \"1\";\n        el.style.transform = `translate3d(0, ${thrY.toFixed(2)}px, 0) translateY(calc(-100% - 8px))`;\n      } else {\n        el.style.opacity = \"0\";\n      }\n    }\n\n    // Runs for the primary series: real values before the newest sample, then the head.\n    const runs: number[][] = [];\n    let run: number[] = [];\n    const lastIdx = newestFinite ? primaryNewest - 1 : primaryNewest;\n    for (let i = primaryStart; i <= lastIdx && i < primary.length; i++) {\n      const s = primary[i];\n      if (s.t > clock) break;\n      if (!Number.isFinite(s.v)) {\n        if (run.length) runs.push(run);\n        run = [];\n        continue;\n      }\n      run.push(xOf(s.t), yOf(s.v));\n    }\n    if (newestFinite && headY !== null) {\n      if (run.length === 0 && runs.length === 0) run.push(plot.l, headY);\n      run.push(plot.r, headY);\n    }\n    if (run.length) runs.push(run);\n    if (runs.length === 1 && runs[0].length === 2) runs[0].unshift(plot.l, runs[0][1]);\n\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(plot.l, plot.t, pW, pH);\n    ctx.clip();\n\n    // Area fill: accent gradient, switching to warning (16%) wherever the line is above the threshold.\n    const accentRgb = hexToRgb(p.accent);\n    const warnRgb = hexToRgb(p.warning);\n    const grad = ctx.createLinearGradient(0, plot.t, 0, plot.b);\n    grad.addColorStop(0, rgba(accentRgb, 0.144));\n    grad.addColorStop(1, rgba(accentRgb, 0));\n    ctx.fillStyle = grad;\n    for (const r of runs) {\n      if (r.length < 4) continue;\n      ctx.beginPath();\n      ctx.moveTo(r[0], r[1]);\n      for (let i = 2; i < r.length; i += 2) ctx.lineTo(r[i], r[i + 1]);\n      ctx.lineTo(r[r.length - 2], plot.b);\n      ctx.lineTo(r[0], plot.b);\n      ctx.closePath();\n      ctx.fill();\n    }\n    if (thrY !== null) {\n      const warnGrad = ctx.createLinearGradient(0, plot.t, 0, plot.b);\n      warnGrad.addColorStop(0, rgba(warnRgb, 0.16));\n      warnGrad.addColorStop(1, rgba(warnRgb, 0));\n      ctx.fillStyle = warnGrad;\n      for (const r of runs) {\n        if (r.length < 4) continue;\n        // Intervals of x where the line is above the threshold (smaller y).\n        const intervals: [number, number][] = [];\n        for (let i = 0; i + 3 < r.length; i += 2) {\n          const x0 = r[i];\n          const y0 = r[i + 1];\n          const x1 = r[i + 2];\n          const y1 = r[i + 3];\n          const a0 = y0 <= thrY!;\n          const a1 = y1 <= thrY!;\n          if (a0 && a1) intervals.push([x0, x1]);\n          else if (a0 !== a1) {\n            const f = (thrY! - y0) / (y1 - y0);\n            const xc = x0 + (x1 - x0) * f;\n            intervals.push(a0 ? [x0, xc] : [xc, x1]);\n          }\n        }\n        for (const [x0, x1] of intervals) {\n          if (x1 - x0 <= 0) continue;\n          ctx.save();\n          ctx.beginPath();\n          ctx.rect(x0, plot.t, x1 - x0, pH);\n          ctx.clip();\n          ctx.beginPath();\n          ctx.moveTo(r[0], r[1]);\n          for (let i = 2; i < r.length; i += 2) ctx.lineTo(r[i], r[i + 1]);\n          ctx.lineTo(r[r.length - 2], plot.b);\n          ctx.lineTo(r[0], plot.b);\n          ctx.closePath();\n          ctx.fill();\n          ctx.restore();\n        }\n      }\n    }\n\n    // Series 2 and 3 are drawn at real values. Series 1 is the accent line.\n    const strokeRuns = (rs: number[][]) => {\n      ctx.beginPath();\n      for (const r of rs) {\n        if (r.length < 4) continue;\n        ctx.moveTo(r[0], r[1]);\n        for (let i = 2; i < r.length; i += 2) ctx.lineTo(r[i], r[i + 1]);\n      }\n      ctx.stroke();\n    };\n    ctx.lineJoin = \"round\";\n    ctx.lineCap = \"round\";\n    for (let bi = 1; bi < buffers.length; bi++) {\n      const b = buffers[bi];\n      const rs: number[][] = [];\n      let cur: number[] = [];\n      const from = Math.max(0, lowerBound(b.samples, wStart) - 1);\n      for (let i = from; i < b.samples.length && b.samples[i].t <= clock; i++) {\n        const s = b.samples[i];\n        if (!Number.isFinite(s.v)) {\n          if (cur.length) rs.push(cur);\n          cur = [];\n          continue;\n        }\n        cur.push(xOf(s.t), yOf(s.v));\n      }\n      if (cur.length) rs.push(cur);\n      ctx.save();\n      if (bi === 1) {\n        ctx.strokeStyle = b.color ?? p.textMuted;\n        ctx.lineWidth = 1.5;\n      } else {\n        ctx.strokeStyle = b.color ?? p.textSoft;\n        ctx.lineWidth = 1.5;\n        ctx.setLineDash([4, 4]);\n      }\n      strokeRuns(rs);\n      ctx.restore();\n    }\n    ctx.lineWidth = 1.75;\n    ctx.strokeStyle = buffers[0]?.color ?? p.accent;\n    strokeRuns(runs);\n\n    ctx.restore();\n\n    // Head dot and halo (halo only while running, never under reduced motion).\n    if (headY !== null && newestFinite) {\n      const running = !isPaused;\n      if (running && !cfg.reduce) {\n        const ph = (t % HALO_PERIOD) / HALO_PERIOD;\n        const e = 1 - Math.pow(1 - ph, 3);\n        const d = 6 + 12 * e;\n        ctx.beginPath();\n        ctx.arc(plot.r, headY, d / 2, 0, Math.PI * 2);\n        ctx.fillStyle = rgba(accentRgb, 0.4 * (1 - ph));\n        ctx.fill();\n      }\n      ctx.beginPath();\n      ctx.arc(plot.r, headY, 3, 0, Math.PI * 2);\n      ctx.fillStyle = p.accent;\n      ctx.fill();\n    }\n\n    // Badge: text from the newest real value, centred on the head y.\n    const badge = badgeRef.current;\n    if (badge) {\n      if (headY !== null && newestVal !== null) {\n        badge.style.opacity = \"1\";\n        badge.style.transform = `translate3d(0, ${(headY - BADGE_H / 2).toFixed(2)}px, 0)`;\n        const text = cfg.formatValue(newestVal);\n        if (st.badgeText !== text) {\n          st.badgeText = text;\n          badge.textContent = text;\n        }\n      } else {\n        badge.style.opacity = \"0\";\n      }\n    }\n\n    // Scrub overlay: snapped crosshair, dot on the line, and a tooltip pill.\n    const tip = tipRef.current;\n    let scrubbing = false;\n    if (st.scrubT !== null && primary.length) {\n      const idx = Math.min(primary.length - 1, Math.max(0, lowerBound(primary, st.scrubT)));\n      let pick = idx;\n      if (idx > 0 && Math.abs(primary[idx - 1].t - st.scrubT) < Math.abs(primary[idx].t - st.scrubT)) pick = idx - 1;\n      const sp = primary[pick];\n      if (sp && sp.t >= wStart && sp.t <= clock) {\n        scrubbing = true;\n        const x = Math.round(xOf(sp.t)) + 0.5;\n        ctx.save();\n        ctx.strokeStyle = p.textSoft;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.moveTo(x, plot.t);\n        ctx.lineTo(x, plot.b);\n        ctx.stroke();\n        if (Number.isFinite(sp.v)) {\n          const y = yOf(sp.v);\n          ctx.beginPath();\n          ctx.arc(x - 0.5, y, 2.5, 0, Math.PI * 2);\n          ctx.fillStyle = p.accent;\n          ctx.fill();\n          ctx.lineWidth = 1.5;\n          ctx.strokeStyle = p.bg;\n          ctx.stroke();\n        }\n        ctx.restore();\n        if (tip) {\n          const text = `${Number.isFinite(sp.v) ? cfg.formatValue(sp.v) : \"No data\"} \\u00b7 ${cfg.formatTime(sp.t)}`;\n          if (st.tipText !== text) {\n            st.tipText = text;\n            tip.textContent = text;\n            st.tipW = tip.offsetWidth;\n          }\n          const tipW = st.tipW;\n          const tipH = 24;\n          const yPoint = Number.isFinite(sp.v) ? yOf(sp.v) : plot.t;\n          let tx = x + 12;\n          if (tx + tipW > w) tx = x - 12 - tipW;\n          const ty = Math.min(Math.max(0, yPoint - tipH - 10), h - tipH);\n          tip.style.opacity = \"1\";\n          tip.style.transform = `translate3d(${tx.toFixed(2)}px, ${ty.toFixed(2)}px, 0)`;\n        }\n      }\n    }\n    if (!scrubbing && tip) tip.style.opacity = \"0\";\n\n    // Summary for assistive tech, refreshed every 5s.\n    const nowMs = performance.now();\n    if (nowMs - st.lastAria >= ARIA_EVERY_MS && newestVal !== null) {\n      st.lastAria = nowMs;\n      canvas.setAttribute(\n        \"aria-label\",\n        `${cfg.ariaLabel}, latest ${cfg.formatValue(newestVal)}, min ${cfg.formatValue(vMin)}, max ${cfg.formatValue(vMax)}, last ${Math.round(windowLen / 1000)} seconds`,\n      );\n    }\n\n    // Keep the loop running for scrub, live data and unsettled state.\n    if (st.scrubT !== null) return true;\n    if (useAttract) return !cfg.reduce;\n    if (isPaused) return !(rangeSettled && headSettled);\n    return hasData;\n  };\n\n  const { wake } = useVisibleLoop(rootRef, frame);\n\n  // Buffers: sorted copies of the props, so out-of-order pushes land in place.\n  useEffect(() => {\n    const list: Buffer[] = [];\n    const sortCopy = (points: LivePoint[]) =>\n      points\n        .filter((p) => Number.isFinite(p.t))\n        .map((p) => ({ t: p.t, v: p.v }))\n        .sort((a, b) => a.t - b.t);\n    if (series && series.length) {\n      series.slice(0, 3).forEach((s) => list.push({ id: s.id, color: s.color, samples: sortCopy(s.data) }));\n    } else if (data) {\n      list.push({ id: \"data\", samples: sortCopy(data) });\n    }\n    bufRef.current = list;\n    cfgRef.current.hasInput = list.length > 0;\n    wake();\n  }, [data, series, attract, wake]);\n\n  // Keep the config and palette the loop reads in sync with the latest render.\n  useEffect(() => {\n    cfgRef.current = {\n      ...cfgRef.current,\n      windowMs,\n      easing,\n      yDomain,\n      threshold,\n      formatValue,\n      formatTime,\n      scrub,\n      onScrub,\n      ariaLabel,\n      reduce,\n      attract,\n    };\n    palRef.current = pal;\n    pausedRef.current = paused;\n    wake();\n  }, [windowMs, easing, yDomain, threshold, formatValue, formatTime, scrub, onScrub, ariaLabel, reduce, attract, pal, paused, wake]);\n\n  // Canvas backing store follows the root size.\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas || !size.width || !size.height) return;\n    sizeRef.current = { w: size.width, h: size.height, dpr: sizeCanvas(canvas, size.width, size.height) };\n    wake();\n  }, [size.width, size.height, wake]);\n\n\n  const markInput = () => {\n    stRef.current.lastInput = performance.now();\n  };\n\n  const setPaused = (next: boolean) => {\n    if (pausedProp === undefined) setPausedState(next);\n    onPausedChange?.(next);\n    wake();\n  };\n\n  const scrubToIndex = (index: number, source: \"pointer\" | \"keyboard\") => {\n    const st = stRef.current;\n    const primary = st.primary;\n    if (!scrub || !primary.length) return;\n    const i = Math.min(primary.length - 1, Math.max(0, index));\n    st.scrubT = primary[i].t;\n    st.scrubSource = source;\n    onScrub?.(i, primary[i]);\n    wake();\n  };\n\n  const clearScrub = () => {\n    const st = stRef.current;\n    st.scrubT = null;\n    st.scrubSource = null;\n    wake();\n  };\n\n  const announceScrub = (index: number) => {\n    const primary = stRef.current.primary;\n    const point = primary[index];\n    if (!point) return;\n    const text = `${cfgRef.current.formatValue(point.v)}, ${cfgRef.current.formatTime(point.t)}`;\n    const now = performance.now();\n    if (announceTimer.current) clearTimeout(announceTimer.current);\n    if (now - announceAt.current >= 250) {\n      announceAt.current = now;\n      setAnnounce(text);\n    } else {\n      announceTimer.current = setTimeout(() => {\n        announceAt.current = performance.now();\n        setAnnounce(text);\n      }, 250);\n    }\n  };\n\n  const indexOfT = (t: number) => {\n    const primary = stRef.current.primary;\n    const i = lowerBound(primary, t);\n    return Math.min(primary.length - 1, i);\n  };\n\n  const onPointerEnter = () => {\n    markInput();\n    const el = wrapperRef.current;\n    if (el) rectRef.current = el.getBoundingClientRect();\n    if (leaveTimer.current) clearTimeout(leaveTimer.current);\n  };\n\n  const pointerToIndex = (e: PointerEvent<HTMLDivElement>): number | null => {\n    const rect = rectRef.current;\n    const st = stRef.current;\n    if (!rect) return null;\n    const x = e.clientX - rect.left;\n    if (x < st.plot.l || x > st.plot.r) return null;\n    const count = st.hitCount;\n    if (!count) return null;\n    let lo = 0;\n    let hi = count - 1;\n    while (lo < hi) {\n      const mid = (lo + hi) >>> 1;\n      if (st.hitX[mid] < x) lo = mid + 1;\n      else hi = mid;\n    }\n    if (lo > 0 && Math.abs(st.hitX[lo - 1] - x) < Math.abs(st.hitX[lo] - x)) lo -= 1;\n    return st.hitIdx[lo];\n  };\n\n  const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    markInput();\n    if (!scrub) return;\n    const index = pointerToIndex(e);\n    if (index === null) return;\n    scrubToIndex(index, \"pointer\");\n    announceScrub(index);\n  };\n\n  const onPointerLeave = () => {\n    if (!scrub) return;\n    if (leaveTimer.current) clearTimeout(leaveTimer.current);\n    leaveTimer.current = setTimeout(() => {\n      if (stRef.current.scrubSource === \"pointer\") clearScrub();\n    }, 120);\n  };\n\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    markInput();\n    if (e.target !== e.currentTarget) return; // the pause button handles its own keys\n    if (e.key === \" \" || e.code === \"Space\") {\n      e.preventDefault();\n      setPaused(!paused);\n      return;\n    }\n    if (!scrub) return;\n    const st = stRef.current;\n    if (e.key === \"ArrowLeft\" || e.key === \"ArrowRight\") {\n      const primary = st.primary;\n      if (!primary.length) return;\n      e.preventDefault();\n      const step = (e.key === \"ArrowLeft\" ? -1 : 1) * (e.shiftKey ? 10 : 1);\n      const current = st.scrubT !== null ? indexOfT(st.scrubT) : primary.length - 1;\n      const next = Math.min(primary.length - 1, Math.max(0, current + step));\n      scrubToIndex(next, \"keyboard\");\n      announceScrub(next);\n    } else if (e.key === \"End\") {\n      if (st.scrubT === null) return;\n      e.preventDefault();\n      clearScrub();\n      setAnnounce(\"Live\");\n    } else if (e.key === \"Escape\") {\n      if (st.scrubT === null) return;\n      e.preventDefault();\n      clearScrub();\n    }\n  };\n\n  const isEmpty = !attract && (series ? series.every((s) => s.data.length === 0) : !data?.length);\n  const showLegend = !!series && series.length > 1;\n  const legend = (series ?? []).slice(0, 3);\n  const thresholdText = threshold ? (threshold.label ?? formatValue(threshold.value)) : \"\";\n\n  const cssVars = {\n    \"--bjork-accent\": pal.accent,\n    \"--bjork-accent-fill\": pal.accentFill,\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-border\": pal.border,\n    \"--bjork-surface-hover\": pal.raised,\n    \"--bjork-ring-offset\": pal.bg,\n  } as CSSProperties;\n\n  return (\n    <div\n      ref={rootRef}\n      data-loop=\"idle\"\n      className={cn(\"relative w-full select-none text-[color:var(--bjork-text)]\", className)}\n      style={{ ...cssVars, height }}\n    >\n      <div\n        ref={wrapperRef}\n        role=\"group\"\n        aria-label={ariaLabel}\n        tabIndex={0}\n        onPointerEnter={onPointerEnter}\n        onPointerMove={onPointerMove}\n        onPointerLeave={onPointerLeave}\n        onPointerDown={(e) => {\n          if (e.pointerType === \"touch\") onPointerMove(e);\n        }}\n        onKeyDown={onKeyDown}\n        onFocus={markInput}\n        className=\"absolute inset-0 touch-pan-y rounded-[10px] 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        <canvas\n          ref={canvasRef}\n          role=\"img\"\n          aria-label={ariaLabel}\n          className=\"pointer-events-none absolute left-0 top-0\"\n        />\n\n        {showLegend && (\n          <div className=\"pointer-events-none absolute flex items-center gap-4 font-bjork-alpha text-[11px] font-medium leading-3\" style={{ left: PAD_LEFT, top: 0 }}>\n            {legend.map((s, i) => (\n              <span key={s.id} className=\"inline-flex items-center gap-1.5 text-[color:var(--bjork-text-medium)]\">\n                <span\n                  aria-hidden=\"true\"\n                  className=\"inline-block h-px w-2\"\n                  style={{\n                    background: s.color ?? (i === 0 ? \"var(--bjork-accent)\" : i === 1 ? \"var(--bjork-text-muted)\" : \"var(--bjork-text-soft)\"),\n                    height: i === 2 ? 0 : 2,\n                    borderTop: i === 2 ? \"1.5px dashed var(--bjork-text-soft)\" : undefined,\n                  }}\n                />\n                {s.label}\n              </span>\n            ))}\n          </div>\n        )}\n\n        {Array.from({ length: 4 }).map((_, i) => (\n          <span\n            key={i}\n            ref={(el) => {\n              gridLabelRefs.current[i] = el;\n            }}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute left-0 top-0 block whitespace-nowrap font-mono text-[10px] leading-none tabular-nums text-[color:var(--bjork-text-faint)] opacity-0 [text-box:trim-both_cap_alphabetic]\"\n          />\n        ))}\n\n        {threshold && (\n          <span\n            ref={thresholdRef}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute right-[72px] top-0 whitespace-nowrap font-mono text-[10px] leading-none tabular-nums text-[color:var(--bjork-text-muted)] opacity-0 [text-box:trim-both_cap_alphabetic]\"\n            style={{ transform: \"translate3d(0, -100px, 0)\" }}\n          >\n            {thresholdText}\n          </span>\n        )}\n\n        <span\n          ref={badgeRef}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute right-0 top-0 inline-flex h-[22px] items-center rounded-[11px] bg-[color:var(--bjork-accent-fill)] px-2 font-mono text-[12px] leading-none tabular-nums text-[color:var(--bjork-accent-foreground)] opacity-0 [text-box:trim-both_cap_alphabetic]\"\n        />\n\n        <div\n          ref={tipRef}\n          role=\"presentation\"\n          className={`pointer-events-none absolute left-0 top-0 flex h-6 items-center whitespace-nowrap rounded-[8px] ${chartCardEdge} bg-[color:var(--bjork-surface-hover)] px-2 font-mono text-[11px] leading-none tabular-nums text-[color:var(--bjork-text)] opacity-0`}\n        />\n\n        {showPauseButton ? (\n          <button\n            type=\"button\"\n            aria-pressed={paused}\n            aria-label={paused ? \"Resume\" : \"Pause\"}\n            onClick={() => setPaused(!paused)}\n            className=\"absolute right-0 top-0 inline-flex size-6 items-center justify-center rounded-[7px] text-[color:var(--bjork-text-muted)] transition-[background-color,color,transform] duration-150 ease-out hover:bg-[color:var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)] focus-visible: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)] active:scale-[0.97]\"\n          >\n            {paused ? (\n              // Play glyph nudged 0.5px right: the triangle's weight sits on its flat left edge (OPTICAL-ALIGNMENT R2, 12px icon).\n              <Play className=\"size-3 translate-x-[0.5px] fill-current\" aria-hidden=\"true\" />\n            ) : (\n              <Pause className=\"size-3 fill-current\" aria-hidden=\"true\" />\n            )}\n          </button>\n        ) : null}\n\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute left-0 right-0 bottom-0 font-mono text-[10px] leading-none tabular-nums text-[color:var(--bjork-text-faint)] [text-box:trim-both_cap_alphabetic]\"\n          style={{ left: PAD_LEFT, right: PAD_RIGHT, height: PAD_BOTTOM }}\n        >\n          <div className=\"flex h-full items-start justify-between pt-[7px]\">\n            <span>-{formatWindow(windowMs)}</span>\n            <span>-{formatWindow(windowMs / 2)}</span>\n            <span>now</span>\n          </div>\n        </div>\n\n        {isEmpty && (\n          <div\n            className=\"pointer-events-none absolute inset-0 flex items-center justify-center font-bjork-alpha text-[12px] text-[color:var(--bjork-text-soft)]\"\n            style={{ top: PAD_TOP, bottom: PAD_BOTTOM, left: PAD_LEFT, right: PAD_RIGHT }}\n          >\n            Waiting for data\n          </div>\n        )}\n      </div>\n\n      <LiveRegion message={announce} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/charts/live-line.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/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/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/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/_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/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/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/live-line.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/live-line.json"
  },
  "type": "registry:component"
}