{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "margin-note",
  "type": "registry:ui",
  "title": "Margin Note",
  "description": "A line chart that reads itself. A reader sweeps across the series and inks it as it goes; wherever something happened it stops, marks the point and writes a note in the margin, level with the point and tied to it by a leader. Hover a note and its stretch of the chart lights up.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "chart",
    "data",
    "ai"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/margin-note",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/margin-note.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { animate, motion, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - The read is one motion `animate` whose `onUpdate` writes the clip, the glow and the reader's\n *   transforms straight to the DOM; React re-renders only when the reader passes a noted point.\n * - The reader holds at each noted point, the holds never take more than `holdShare` of `duration`,\n *   and between them it sweeps at one steady, linear pace.\n * - Every note is rendered from the start, so the margin holds its room, and each is placed level\n *   with its point once measured; nothing shifts when the notes arrive.\n * - Inputs are keyed by content (`dataKey`, `notesKey`), so a re-render with equal inline arrays\n *   doesn't restart the read; new data or notes read again from the start.\n * - Under reduced motion or `play={false}` nothing sweeps: the chart arrives read with every note\n *   in place, and highlights and the crosshair still work.\n */\nconst TIMING = {\n  /** The whole read, pauses included, in ms (the `duration` prop). */\n  duration: 2400,\n  /** The reader stopping at each noted point, in ms (the `hold` prop). */\n  hold: 280,\n  /** The shortest a read can take, in ms; a shorter `duration` is raised to it. */\n  shortest: 400,\n  /** The reader's pace between holds. */\n  sweepEase: \"linear\",\n  /** A noted point popping in. */\n  pop: { type: \"spring\", visualDuration: 0.3, bounce: 0.25 },\n  /** Its ring spreading out and fading, in seconds. */\n  ring: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },\n  /** The leader drawing out to the margin, in seconds. */\n  leader: { duration: 0.32, ease: [0.22, 1, 0.36, 1], delay: 0.04 },\n  /** The note sliding in behind it, after a delay in seconds. */\n  note: { type: \"spring\", visualDuration: 0.3, bounce: 0, delay: 0.1 },\n  /** Between summary words, in seconds. */\n  word: 0.035,\n  /** The longest the summary's stagger runs, in seconds. */\n  wordSpread: 0.3,\n  /** A summary word rising. */\n  rise: { type: \"spring\", visualDuration: 0.3, bounce: 0 },\n  /** The reader fading once the read ends, in seconds. */\n  reader: { duration: 0.25, ease: \"easeOut\" },\n  /** Highlights and dimming, in seconds. */\n  light: { duration: 0.15, ease: \"easeOut\" },\n  /** The same highlights where CSS runs them: a point's halo, a leader's stroke, a note dimming. */\n  lightClass: \"duration-150\",\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * MARGIN NOTE STORYBOARD\n *\n *  rest     the chart's frame: four hairlines with their values,\n *           a few dates along the bottom and the series in a\n *           faint print, not read yet. The margin beside it is\n *           empty but already holds its room, so nothing moves\n *           when the notes arrive\n *  view     35% in view, a reader (a hairline, a dot riding the\n *           series and a soft glow behind it) sweeps left to\n *           right at a steady pace, inking the line and its fill\n *           as it passes. 2.4s for the whole read, pauses\n *           included\n *  note     at a noted point the reader stops for 280ms, as if\n *           writing. The point pops (scale 0.6 → 1 with a ring\n *           spreading out and fading), the leader draws from it\n *           to the margin in 0.32s and the note slides 8px in\n *           behind it. Then the reader moves on. A note about a\n *           stretch shades its band as the reader crosses it and\n *           is written at the stretch's end\n *  margin   each note sits level with its point, tied to it by a\n *           dotted leader, so the eye runs straight across.\n *           Crowded notes step out of each other's way and their\n *           leaders bend to reach them\n *  summary  the read ends, the reader fades (0.25s) and the\n *           takeaway above the chart rises in word by word, 35ms\n *           apart\n *  link     hover or focus a note: its point rings, its band\n *           shades, its leader firms up and the other notes fade\n *           to half. Hover the chart: a crosshair snaps to the\n *           nearest point with its label and value, and lights\n *           the note there if it has one\n *  again    new data or notes read again from the start\n *  narrow   under 34rem the margin drops under the chart as a\n *           numbered list, the points carry the numbers and the\n *           leaders go\n *  touch    tap a note to light it, tap it again to clear. Drag\n *           across the chart to move the crosshair\n *  keys     the chart takes focus: ← → step the crosshair point\n *           by point, Home / End jump to the ends, Esc clears.\n *           Notes are buttons in reading order. The value under\n *           the crosshair is announced\n *  reduced  no sweep: the chart arrives read, every note in\n *           place. Highlights and the crosshair work, unanimated\n * ───────────────────────────────────────────────────────── */\n\nconst MARGIN = {\n  holdShare: 0.45, // the pauses never take more than this share of the read\n  view: 0.35, // share of the chart in view before it reads\n  bend: 14, // px a leader runs before it turns toward a note that had to step aside\n  slide: 8, // px a note slides in from\n  lift: 6, // px a summary word rises from\n  dim: 0.45, // opacity of the other notes while one is lit\n  glow: 56, // px of glow trailing the reader\n  gap: 10, // px between notes stacked in the margin\n  hang: 28, // px a note may hang below the plot, beside the labels, before the stack lifts\n  badge: 9, // px from a note's top to its number's middle, level with the point\n} as const;\n\n/* ── Public types ───────────────────────────────────────── */\n\nexport interface MarginNoteNote {\n  /** Index in `data` of the point the note is about. */\n  at: number;\n  /** Index where a note about a stretch of time ends. The band between `at` and `to` shades. */\n  to?: number;\n  /** The note's first line. Keep it short. */\n  title: string;\n  /** One line of detail under the title. */\n  body?: string;\n  /** A small figure beside the title, like \"+38%\". */\n  delta?: string;\n}\n\nexport interface MarginNoteProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** The series, one value per point, oldest first. */\n  data: number[];\n  /** A label per point, like a date. A few run under the chart, and the crosshair shows the one it's on. */\n  labels?: string[];\n  /** What the reader writes in the margin. Each note points at an index in `data`; they're numbered in reading order. */\n  notes?: MarginNoteNote[];\n  /** The takeaway above the chart, written once the read ends. */\n  summary?: React.ReactNode;\n  /** Formats values on the axis and under the crosshair. Defaults to compact numbers (1.2K). */\n  format?: (value: number) => string;\n  /** A second series to compare against, like last period. Drawn dashed and never read. */\n  compare?: number[];\n  /** `\"view\"` reads once the chart is 35% in view. `true` reads now, and again each time it flips back to `true`. `false` shows it already read. */\n  play?: boolean | \"view\";\n  /** How long the read takes, in ms, pauses at the notes included. */\n  duration?: number;\n  /** How long the reader stops at each noted point, in ms, as if writing. The pauses never take more than 45% of `duration`. */\n  hold?: number;\n  /** What the chart shows, like \"Weekly signups\". Screen readers hear it first. */\n  label?: string;\n  /** Called when a read ends. */\n  onComplete?: () => void;\n}\n\n/* ── Geometry ───────────────────────────────────────────── */\n\n// Everything is worked out in a unit square (x right, y down) and scaled to the measured box at\n// draw time, so a resize never needs the data again and strokes stay a true 1.5px.\ntype Point = { x: number; y: number };\ntype Segment = { x0: number; y0: number; c1: number; c2: number; x1: number; y1: number };\ntype Series = { points: Point[]; segments: Segment[] };\ntype Geometry = { min: number; max: number; ticks: number[]; zero: number | null; main: Series; compare: Series | null };\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst round = (v: number) => Math.round(v * 100) / 100;\nconst compact = new Intl.NumberFormat(\"en\", { notation: \"compact\", maximumFractionDigits: 1 });\nconst formatCompact = (value: number) => compact.format(value);\nconst noopSubscribe = () => () => {};\nconst NO_NOTES: MarginNoteNote[] = [];\n\n/** Round numbers for the hairlines, a step of 1, 2, 2.5 or 5 × 10ⁿ, with a little air above the peak. */\nfunction niceScale(values: number[]) {\n  let lo = Math.min(...values);\n  let hi = Math.max(...values);\n  if (!Number.isFinite(lo)) return { min: 0, max: 1, ticks: [0, 1] };\n  if (lo === hi) {\n    const pad = Math.abs(lo) * 0.1 || 1;\n    lo -= pad;\n    hi += pad;\n  }\n  const span = hi - lo;\n  hi += span * 0.08;\n  lo = lo >= 0 ? Math.max(0, lo - span * 0.04) : lo - span * 0.04;\n  const raw = (hi - lo) / 4;\n  const mag = 10 ** Math.floor(Math.log10(raw));\n  const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= raw) ?? raw;\n  const min = Math.floor(lo / step) * step;\n  const max = Math.ceil(hi / step) * step;\n  const ticks: number[] = [];\n  for (let v = min; v <= max + step / 2; v += step) ticks.push(Number(v.toPrecision(12)));\n  return { min, max, ticks };\n}\n\n/** A monotone cubic through the points (Fritsch–Carlson), so the curve never overshoots a value. */\nfunction monotone(points: Point[]): Segment[] {\n  const n = points.length;\n  if (n < 2) return [];\n  const d: number[] = [];\n  for (let i = 0; i < n - 1; i++) d.push((points[i + 1].y - points[i].y) / (points[i + 1].x - points[i].x));\n  const m: number[] = new Array(n);\n  m[0] = d[0];\n  m[n - 1] = d[n - 2];\n  for (let i = 1; i < n - 1; i++) m[i] = d[i - 1] * d[i] <= 0 ? 0 : (d[i - 1] + d[i]) / 2;\n  for (let i = 0; i < n - 1; i++) {\n    if (d[i] === 0) {\n      m[i] = 0;\n      m[i + 1] = 0;\n      continue;\n    }\n    const a = m[i] / d[i];\n    const b = m[i + 1] / d[i];\n    const s = a * a + b * b;\n    if (s > 9) {\n      const t = 3 / Math.sqrt(s);\n      m[i] = t * a * d[i];\n      m[i + 1] = t * b * d[i];\n    }\n  }\n  return points.slice(0, -1).map((p, i) => {\n    const q = points[i + 1];\n    const dx = q.x - p.x;\n    return { x0: p.x, y0: p.y, c1: p.y + (m[i] * dx) / 3, c2: q.y - (m[i + 1] * dx) / 3, x1: q.x, y1: q.y };\n  });\n}\n\nfunction series(values: number[], min: number, max: number): Series {\n  const n = values.length;\n  const points = values.map((v, i) => ({ x: n === 1 ? 0.5 : i / (n - 1), y: (max - v) / (max - min) }));\n  return { points, segments: monotone(points) };\n}\n\nfunction buildGeometry(data: number[], compare: number[] | null): Geometry {\n  const { min, max, ticks } = niceScale(compare ? [...data, ...compare] : data);\n  return {\n    min,\n    max,\n    ticks,\n    zero: min < 0 && max > 0 ? max / (max - min) : null,\n    main: series(data, min, max),\n    compare: compare && compare.length ? series(compare, min, max) : null,\n  };\n}\n\n/** The series' height at a point across, exact on the drawn curve: its x runs linearly in each segment. */\nfunction heightAt({ points, segments }: Series, fx: number) {\n  if (!segments.length) return points[0]?.y ?? 0.5;\n  const i = clamp(Math.floor(fx * segments.length), 0, segments.length - 1);\n  const s = segments[i];\n  const t = clamp((fx - s.x0) / (s.x1 - s.x0), 0, 1);\n  const u = 1 - t;\n  return u * u * u * s.y0 + 3 * u * u * t * s.c1 + 3 * u * t * t * s.c2 + t * t * t * s.y1;\n}\n\nfunction linePath({ points, segments }: Series, w: number, h: number) {\n  if (!points.length) return \"\";\n  if (!segments.length) return `M${round(points[0].x * w - 0.5)},${round(points[0].y * h)}h1`;\n  let d = `M${round(segments[0].x0 * w)},${round(segments[0].y0 * h)}`;\n  for (const s of segments) {\n    const third = (s.x1 - s.x0) / 3;\n    d += `C${round((s.x0 + third) * w)},${round(s.c1 * h)},${round((s.x1 - third) * w)},${round(s.c2 * h)},${round(s.x1 * w)},${round(s.y1 * h)}`;\n  }\n  return d;\n}\n\nfunction areaPath(s: Series, base: number, w: number, h: number) {\n  if (s.segments.length === 0) return \"\";\n  const first = s.points[0];\n  const last = s.points[s.points.length - 1];\n  return `${linePath(s, w, h)}L${round(last.x * w)},${round(base * h)}L${round(first.x * w)},${round(base * h)}Z`;\n}\n\n/** Which labels fit along the bottom: evenly stepped from the first, plus the last when it has room. */\nfunction labelIndices(labels: string[] | undefined, n: number, width: number) {\n  if (!labels?.length || n === 0) return [];\n  if (n === 1) return [0];\n  const longest = Math.max(...labels.map((l) => l.length));\n  const room = longest * 6.4 + 20; // px one label needs at 11px, with air\n  const fit = clamp(Math.floor(width / room), 2, 8);\n  const step = Math.max(1, Math.ceil((n - 1) / (fit - 1)));\n  const picked: number[] = [];\n  for (let i = 0; i < n; i += step) picked.push(i);\n  const last = n - 1;\n  if (picked[picked.length - 1] !== last) {\n    // The last label always shows; it takes the place of a neighbor that would crowd it.\n    if (last - picked[picked.length - 1] < step * 0.6) picked.pop();\n    picked.push(last);\n  }\n  return picked;\n}\n\n/* ── Notes in reading order ─────────────────────────────── */\n\ntype Note = MarginNoteNote & {\n  index: number; // position in reading order, from 0\n  anchor: number; // the point the note is written at: the end of its stretch, or its point\n  from: number; // the stretch's start (equals anchor for a point)\n};\n\nfunction readingOrder(notes: MarginNoteNote[], n: number): Note[] {\n  if (n === 0) return [];\n  return notes\n    .map((note) => {\n      const a = clamp(Math.round(note.at), 0, n - 1);\n      const b = note.to === undefined ? a : clamp(Math.round(note.to), 0, n - 1);\n      return { ...note, from: Math.min(a, b), anchor: Math.max(a, b) };\n    })\n    .sort((p, q) => p.anchor - q.anchor || p.from - q.from)\n    .map((note, index) => ({ ...note, index }));\n}\n\n/* ── Layout, measured ───────────────────────────────────── */\n\ntype Layout = {\n  w: number; // the plot's size in px\n  h: number;\n  plotX: number; // the plot's offset inside the body\n  plotY: number;\n  wide: boolean; // the margin beside the chart (34rem and up), not under it\n  column: { x: number; y: number; h: number } | null; // the margin column, when its notes are placed by hand\n  tops: number[]; // each note's top inside the column, in reading order\n};\n\nconst sameLayout = (a: Layout | null, b: Layout) =>\n  !!a &&\n  a.w === b.w &&\n  a.h === b.h &&\n  a.plotX === b.plotX &&\n  a.plotY === b.plotY &&\n  a.wide === b.wide &&\n  a.column?.x === b.column?.x &&\n  a.column?.y === b.column?.y &&\n  a.column?.h === b.column?.h &&\n  a.tops.length === b.tops.length &&\n  a.tops.every((t, i) => t === b.tops[i]);\n\n/** Puts each note level with its point, then steps crowded ones down (and back up from the floor). */\nfunction placeNotes(want: number[], heights: number[], floor: number, gap: number) {\n  const order = want.map((_, i) => i).sort((a, b) => want[a] - want[b]);\n  const tops = new Array<number>(want.length);\n  let cursor = -Infinity;\n  for (const i of order) {\n    tops[i] = Math.max(want[i], cursor, 0);\n    cursor = tops[i] + heights[i] + gap;\n  }\n  // Pushed past the floor? Lift the stack back up, as far as the top allows.\n  let limit = floor;\n  for (let k = order.length - 1; k >= 0; k--) {\n    const i = order[k];\n    if (tops[i] + heights[i] > limit) tops[i] = Math.max(0, limit - heights[i]);\n    limit = tops[i] - gap;\n  }\n  return tops.map(Math.round);\n}\n\n/* ── MarginNote ─────────────────────────────────────────── */\n\ntype Run = { token: number; phase: \"idle\" | \"reading\" | \"done\"; shown: number };\nconst startRun = (r: Run): Run => ({ token: r.token + 1, phase: \"reading\", shown: 0 });\n\nexport function MarginNote({\n  data,\n  labels,\n  notes = NO_NOTES,\n  summary,\n  format = formatCompact,\n  compare,\n  play = \"view\",\n  duration = TIMING.duration,\n  hold: holdProp = TIMING.hold,\n  label = \"Chart\",\n  onComplete,\n  className,\n  ...props\n}: MarginNoteProps) {\n  const uid = React.useId().replace(/[^a-zA-Z0-9-]/g, \"\");\n  const hydrated = React.useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const reduced = (useReducedMotion() ?? false) && hydrated;\n\n  // Content keys, so inline arrays don't restart the read on every render.\n  const dataKey = data.join(\",\");\n  const compareKey = compare?.join(\",\") ?? \"\";\n  const notesKey = JSON.stringify(notes);\n  const geo = React.useMemo(\n    () =>\n      buildGeometry(\n        dataKey ? dataKey.split(\",\").map(Number) : [],\n        compareKey ? compareKey.split(\",\").map(Number) : null,\n      ),\n    [dataKey, compareKey],\n  );\n  const n = geo.main.points.length;\n  const order = React.useMemo(() => readingOrder(JSON.parse(notesKey) as MarginNoteNote[], n), [notesKey, n]);\n\n  /* the run: idle → reading → done */\n  const [run, setRun] = React.useState<Run>({ token: 0, phase: \"idle\", shown: 0 });\n  const contentKey = `${dataKey}|${notesKey}`;\n  const [seenKey, setSeenKey] = React.useState(contentKey);\n  const [seenPlay, setSeenPlay] = React.useState<MarginNoteProps[\"play\"] | null>(null);\n  if (seenKey !== contentKey) {\n    // New data reads again from the start (in view: right away, see the observer below).\n    setSeenKey(contentKey);\n    setRun((r) => (play === true ? startRun(r) : { token: r.token + 1, phase: \"idle\", shown: 0 }));\n  }\n  if (seenPlay !== play) {\n    setSeenPlay(play);\n    if (play === true) setRun(startRun);\n    else if (play === \"view\" && seenPlay !== null) setRun((r) => ({ token: r.token + 1, phase: \"idle\", shown: 0 }));\n  }\n\n  const still = play === false || reduced; // never sweeps: arrives read\n  const phase = play === false ? \"done\" : run.phase;\n  const done = phase === \"done\" || still;\n  const shown = done ? order.length : run.shown;\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const bodyRef = React.useRef<HTMLDivElement>(null);\n  const plotRef = React.useRef<HTMLDivElement>(null);\n  const columnRef = React.useRef<HTMLOListElement>(null);\n  const clipRef = React.useRef<SVGRectElement>(null);\n  const glowRef = React.useRef<SVGRectElement>(null);\n  const readerRef = React.useRef<HTMLDivElement>(null);\n  const readerDotRef = React.useRef<HTMLDivElement>(null);\n  const layoutRef = React.useRef<Layout | null>(null);\n  const callbacks = React.useRef({ onComplete });\n  React.useEffect(() => {\n    callbacks.current = { onComplete };\n  });\n\n  /* measure: the plot in px, and where each note sits in the margin */\n  const [layout, setLayout] = React.useState<Layout | null>(null);\n  React.useEffect(() => {\n    layoutRef.current = layout;\n  }, [layout]);\n\n  const anchorsKey = order.map((note) => note.anchor).join(\",\");\n  React.useEffect(() => {\n    const root = rootRef.current;\n    const body = bodyRef.current;\n    const plot = plotRef.current;\n    if (!root || !body || !plot) return;\n    const anchors = anchorsKey ? anchorsKey.split(\",\").map(Number) : [];\n    const measure = () => {\n      const b = body.getBoundingClientRect();\n      const p = plot.getBoundingClientRect();\n      const column = columnRef.current;\n      const c = column?.getBoundingClientRect();\n      const wide = !!c && c.left >= p.right - 1; // the margin sits beside the plot, not under it\n      const next: Layout = {\n        w: Math.round(p.width),\n        h: Math.round(p.height),\n        plotX: Math.round(p.left - b.left),\n        plotY: Math.round(p.top - b.top),\n        wide,\n        column: null,\n        tops: [],\n      };\n      if (wide && column && c && anchors.length) {\n        const items = Array.from(column.children) as HTMLElement[];\n        const heights = items.map((item) => item.offsetHeight);\n        const want = anchors.map(\n          (a) => next.plotY - (c.top - b.top) + geo.main.points[a].y * next.h - MARGIN.badge,\n        );\n        const floor = Math.max(next.h + MARGIN.hang, heights.reduce((sum, h) => sum + h + MARGIN.gap, 0));\n        const tops = placeNotes(want, heights, floor, MARGIN.gap);\n        const bottom = Math.max(...tops.map((t, i) => t + heights[i]));\n        next.column = { x: Math.round(c.left - b.left), y: Math.round(c.top - b.top), h: Math.max(next.h, bottom) };\n        next.tops = tops;\n      }\n      setLayout((prev) => (sameLayout(prev, next) ? prev : next));\n    };\n    const observer = new ResizeObserver(measure);\n    observer.observe(root);\n    observer.observe(plot);\n    if (columnRef.current) {\n      observer.observe(columnRef.current);\n      for (const item of Array.from(columnRef.current.children)) observer.observe(item);\n    }\n    let alive = true;\n    document.fonts?.ready.then(() => alive && measure());\n    return () => {\n      alive = false;\n      observer.disconnect();\n    };\n  }, [anchorsKey, geo]);\n\n  /* play once 35% in view */\n  const auto = play === \"view\";\n  React.useEffect(() => {\n    const root = rootRef.current;\n    if (!auto || !root || run.phase !== \"idle\") return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry || entry.intersectionRatio < MARGIN.view - 0.01) return;\n        observer.disconnect();\n        setRun((r) => (r.phase === \"idle\" ? startRun(r) : r));\n      },\n      { threshold: MARGIN.view },\n    );\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, [auto, run.phase, run.token]);\n\n  /* the read */\n  const stopsKey = order.map((note) => note.anchor).join(\",\");\n  React.useEffect(() => {\n    if (run.phase !== \"reading\" || play === false) return;\n    const token = run.token;\n    const finish = () => {\n      setRun((r) => (r.token === token && r.phase === \"reading\" ? { ...r, phase: \"done\", shown: Infinity } : r));\n      callbacks.current.onComplete?.();\n    };\n    if (still) {\n      const id = window.setTimeout(finish, 0);\n      return () => window.clearTimeout(id);\n    }\n\n    // The reader's path through time: steady sweeps, with a hold at each noted point.\n    const count = Math.max(1, n - 1);\n    const marks = stopsKey ? [...new Set(stopsKey.split(\",\").map(Number))].map((a) => a / count) : [];\n    const total = Math.max(duration, TIMING.shortest);\n    const hold = Math.min(Math.max(0, holdProp), (total * MARGIN.holdShare) / Math.max(1, marks.length));\n    const sweep = total - hold * marks.length;\n    const xs = [0];\n    const ts = [0];\n    let t = 0;\n    let at = 0;\n    for (const mark of marks) {\n      t += (mark - at) * sweep;\n      xs.push(mark, mark);\n      ts.push(t, t + hold);\n      t += hold;\n      at = mark;\n    }\n    xs.push(1);\n    ts.push(t + (1 - at) * sweep);\n    const end = ts[ts.length - 1] || 1;\n    const position = (u: number) => {\n      const time = u * end;\n      for (let i = 1; i < ts.length; i++) {\n        if (time <= ts[i]) {\n          const span = ts[i] - ts[i - 1];\n          return span <= 0 ? xs[i] : xs[i - 1] + ((time - ts[i - 1]) / span) * (xs[i] - xs[i - 1]);\n        }\n      }\n      return 1;\n    };\n    const anchors = stopsKey ? stopsKey.split(\",\").map((a) => Number(a) / count) : [];\n\n    let revealed = 0;\n    const paint = (fx: number) => {\n      const lay = layoutRef.current;\n      if (!lay) return;\n      const x = fx * lay.w;\n      clipRef.current?.setAttribute(\"width\", String(round(x + 1)));\n      glowRef.current?.setAttribute(\"x\", String(round(x - MARGIN.glow)));\n      if (readerRef.current) readerRef.current.style.transform = `translate3d(${round(x)}px,0,0)`;\n      if (readerDotRef.current) {\n        const y = heightAt(geo.main, fx) * lay.h;\n        readerDotRef.current.style.transform = `translate3d(${round(x)}px,${round(y)}px,0)`;\n      }\n      let passed = 0;\n      while (passed < anchors.length && anchors[passed] <= fx + 1e-6) passed++;\n      if (passed > revealed) {\n        revealed = passed;\n        setRun((r) => (r.token === token ? { ...r, shown: passed } : r));\n      }\n    };\n    const controls = animate(0, 1, {\n      duration: end / 1000,\n      ease: TIMING.sweepEase,\n      onUpdate: (u) => paint(position(u)),\n      onComplete: () => {\n        paint(1);\n        finish();\n      },\n    });\n    return () => controls.stop();\n  }, [run.phase, run.token, play, still, n, stopsKey, duration, holdProp, geo]);\n\n  /* lighting a note: hover, focus or a tap on it, or the crosshair landing on its point */\n  const [pinned, setPinned] = React.useState<number | null>(null);\n  const [cross, setCross] = React.useState<number | null>(null);\n  const [spoken, setSpoken] = React.useState(\"\"); // the value the keyboard last stepped to, read out\n  const reading = run.phase === \"reading\" && !still;\n  const crossNote = cross === null ? -1 : order.findIndex((note) => cross >= note.from && cross <= note.anchor);\n  const lit = pinned ?? (crossNote >= 0 && crossNote < shown ? crossNote : null);\n  const litNote = lit === null ? null : order[lit];\n\n  const valueAt = (i: number) => data[i];\n  const describe = (i: number) =>\n    `${labels?.[i] ?? `Point ${i + 1}`}: ${format(valueAt(i))}${compare?.[i] !== undefined ? `, compared with ${format(compare[i])}` : \"\"}`;\n  const indexAt = (clientX: number) => {\n    const rect = plotRef.current?.getBoundingClientRect();\n    if (!rect || n === 0) return null;\n    return Math.round(clamp((clientX - rect.left) / rect.width, 0, 1) * (n - 1));\n  };\n  const follow = (event: React.PointerEvent) => {\n    if (reading) return;\n    if (event.pointerType !== \"mouse\" && event.buttons === 0) return;\n    const i = indexAt(event.clientX);\n    if (i !== cross) setCross(i);\n  };\n  const onKeyDown = (event: React.KeyboardEvent) => {\n    if (n === 0) return;\n    const last = n - 1;\n    const from = cross ?? last;\n    let next: number | null = from;\n    if (event.key === \"ArrowRight\") next = cross === null ? last : Math.min(last, from + 1);\n    else if (event.key === \"ArrowLeft\") next = cross === null ? last : Math.max(0, from - 1);\n    else if (event.key === \"Home\") next = 0;\n    else if (event.key === \"End\") next = last;\n    else if (event.key === \"Escape\") next = null;\n    else return;\n    event.preventDefault();\n    setCross(next);\n    setSpoken(next === null ? \"\" : describe(next));\n    if (event.key === \"Escape\") setPinned(null);\n  };\n\n  /* drawing sizes: measured px, or a stretched unit box until the first measure */\n  const w = layout?.w ?? 1000;\n  const h = layout?.h ?? 1000;\n  const measured = layout !== null;\n  const base = geo.zero ?? 1;\n  const line = linePath(geo.main, w, h);\n  const area = areaPath(geo.main, base, w, h);\n  const ghost = geo.compare ? linePath(geo.compare, w, h) : \"\";\n  const xLabels = labelIndices(labels, n, layout?.w ?? 600);\n  const gutter = `calc(${Math.max(...geo.ticks.map((tick) => format(tick).length))}ch + 0.75rem)`;\n\n  const clipId = `${uid}-read`;\n  const fillId = `${uid}-fill`;\n  const glowId = `${uid}-glow`;\n  const descId = `${uid}-desc`;\n\n  const placed = layout?.wide && layout.column && layout.tops.length === order.length ? layout : null;\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"@container/margin-note relative w-full [--margin-note-accent:#5b54d6] dark:[--margin-note-accent:#a5a0ff]\",\n        className,\n      )}\n      {...props}\n    >\n      {summary !== undefined && <Summary summary={summary} shown={done} instant={still} />}\n\n      <div\n        ref={bodyRef}\n        className={cn(\n          \"relative grid grid-cols-1 gap-x-7 gap-y-5\",\n          order.length > 0 && \"@[34rem]/margin-note:grid-cols-[minmax(0,1fr)_minmax(10rem,13.5rem)]\",\n        )}\n      >\n        <div className=\"min-w-0 pt-2\">\n          <div className=\"flex\">\n            {/* the values, in a gutter so the series never runs through them */}\n            <div\n              aria-hidden\n              className=\"relative shrink-0 text-[11px] leading-4 text-current/55 tabular-nums\"\n              style={{ width: gutter }}\n            >\n              {geo.ticks.map((tick) => (\n                <span\n                  key={tick}\n                  className=\"absolute right-2.5 -translate-y-1/2 whitespace-nowrap\"\n                  style={{ top: `${((geo.max - tick) / (geo.max - geo.min)) * 100}%` }}\n                >\n                  {format(tick)}\n                </span>\n              ))}\n            </div>\n            <div\n              ref={plotRef}\n              role=\"group\"\n              aria-roledescription=\"chart\"\n              tabIndex={0}\n              aria-label={`${label}. Arrow keys read the values.`}\n              aria-describedby={descId}\n              onPointerMove={follow}\n              onPointerDown={(event) => {\n                if (event.pointerType === \"mouse\") return;\n                (event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);\n                const i = indexAt(event.clientX);\n                if (!reading) setCross(i);\n              }}\n              onPointerLeave={(event) => {\n                if (event.pointerType === \"mouse\") setCross(null);\n              }}\n              onKeyDown={onKeyDown}\n              onBlur={() => setCross(null)}\n              className=\"relative h-[var(--margin-note-height,12.5rem)] min-w-0 flex-1 touch-pan-y rounded-[2px] outline-offset-4 outline-[color:var(--margin-note-accent)] select-none focus-visible:outline-2 @[34rem]/margin-note:h-[var(--margin-note-height,15rem)]\"\n            >\n              {/* hairlines */}\n              <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n                {geo.ticks.map((tick) => (\n                  <div\n                    key={tick}\n                    className={cn(\n                      \"absolute inset-x-0 h-px\",\n                      tick === 0 && geo.zero !== null ? \"bg-current/25\" : \"bg-current/[0.08]\",\n                    )}\n                    style={{ top: `${((geo.max - tick) / (geo.max - geo.min)) * 100}%` }}\n                  />\n                ))}\n              </div>\n\n              <svg\n                aria-hidden\n                className=\"pointer-events-none absolute inset-0 h-full w-full overflow-visible\"\n                viewBox={`0 0 ${w} ${h}`}\n                preserveAspectRatio=\"none\"\n              >\n                <defs>\n                  <linearGradient id={fillId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                    <stop offset=\"0\" style={{ stopColor: \"var(--margin-note-accent)\", stopOpacity: 0.2 }} />\n                    <stop offset=\"1\" style={{ stopColor: \"var(--margin-note-accent)\", stopOpacity: 0 }} />\n                  </linearGradient>\n                  <linearGradient id={glowId} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n                    <stop offset=\"0\" style={{ stopColor: \"var(--margin-note-accent)\", stopOpacity: 0 }} />\n                    <stop offset=\"1\" style={{ stopColor: \"var(--margin-note-accent)\", stopOpacity: 0.14 }} />\n                  </linearGradient>\n                  <clipPath id={clipId}>\n                    <rect ref={clipRef} x={-8} y={-16} width={done ? w + 16 : 0} height={h + 32} />\n                  </clipPath>\n                </defs>\n\n                {/* a lit note's stretch, or a narrow column at its point */}\n                {litNote && (\n                  <motion.g\n                    key={`lit-${litNote.index}`}\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    transition={reduced ? { duration: 0 } : TIMING.light}\n                  >\n                    <rect\n                      x={bandX(litNote.from, n, w)}\n                      width={Math.max(bandX(litNote.anchor, n, w, true) - bandX(litNote.from, n, w), 2)}\n                      y={0}\n                      height={h}\n                      style={{ fill: \"var(--margin-note-accent)\", fillOpacity: 0.1 }}\n                    />\n                  </motion.g>\n                )}\n\n                {/* the comparison, printed and never read */}\n                {ghost && (\n                  <path\n                    d={ghost}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeOpacity={0.32}\n                    strokeWidth={1.25}\n                    strokeDasharray=\"3 4\"\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                  />\n                )}\n\n                {/* the series before it's read: a faint print */}\n                <path\n                  d={line}\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeOpacity={done ? 0 : 0.16}\n                  strokeWidth={1.5}\n                  strokeLinejoin=\"round\"\n                  strokeLinecap=\"round\"\n                  vectorEffect=\"non-scaling-stroke\"\n                />\n\n                {/* what the reader has inked */}\n                <g clipPath={`url(#${clipId})`}>\n                  {order.map((note) =>\n                    note.from === note.anchor ? null : (\n                      <rect\n                        key={`band-${note.index}`}\n                        x={bandX(note.from, n, w)}\n                        width={bandX(note.anchor, n, w, true) - bandX(note.from, n, w)}\n                        y={0}\n                        height={h}\n                        style={{ fill: \"var(--margin-note-accent)\", fillOpacity: 0.06 }}\n                      />\n                    ),\n                  )}\n                  {area && <path d={area} fill={`url(#${fillId})`} />}\n                  <path\n                    d={line}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={1.5}\n                    strokeLinejoin=\"round\"\n                    strokeLinecap=\"round\"\n                    vectorEffect=\"non-scaling-stroke\"\n                  />\n                </g>\n\n                {/* the glow trailing the reader */}\n                {measured && (\n                  <motion.g initial={false} animate={{ opacity: reading ? 1 : 0 }} transition={TIMING.reader}>\n                    <rect ref={glowRef} x={-MARGIN.glow} width={MARGIN.glow} y={0} height={h} fill={`url(#${glowId})`} />\n                  </motion.g>\n                )}\n              </svg>\n\n              {/* the reader */}\n              <motion.div\n                aria-hidden\n                initial={false}\n                animate={{ opacity: reading && measured ? 1 : 0 }}\n                transition={TIMING.reader}\n                className=\"pointer-events-none absolute inset-0\"\n              >\n                <div ref={readerRef} className=\"absolute inset-y-0 left-0 w-px bg-[var(--margin-note-accent)]/60\" />\n                <div ref={readerDotRef} className=\"absolute top-0 left-0\">\n                  <span className=\"absolute -top-1 -left-1 size-2 rounded-full bg-[var(--margin-note-accent)] shadow-[0_0_0_4px_color-mix(in_oklab,var(--margin-note-accent)_22%,transparent)]\" />\n                </div>\n              </motion.div>\n\n              {/* noted points */}\n              {order.map((note) => {\n                const p = geo.main.points[note.anchor];\n                const on = note.index < shown;\n                return (\n                  <div\n                    key={`point-${note.index}`}\n                    aria-hidden\n                    className=\"pointer-events-none absolute\"\n                    style={{ left: `${p.x * 100}%`, top: `${p.y * 100}%` }}\n                  >\n                    {on && !still && (\n                      <motion.span\n                        key={`ring-${run.token}`}\n                        initial={{ opacity: 0.55, transform: \"translate(-50%, -50%) scale(1)\" }}\n                        animate={{ opacity: 0, transform: \"translate(-50%, -50%) scale(3.2)\" }}\n                        transition={TIMING.ring}\n                        className=\"absolute top-0 left-0 size-3 rounded-full border border-[var(--margin-note-accent)]\"\n                      />\n                    )}\n                    <motion.span\n                      initial={false}\n                      animate={\n                        on\n                          ? { opacity: 1, transform: \"translate(-50%, -50%) scale(1)\" }\n                          : { opacity: 0, transform: \"translate(-50%, -50%) scale(0.6)\" }\n                      }\n                      transition={still ? { duration: 0 } : TIMING.pop}\n                      className={cn(\n                        `absolute top-0 left-0 flex items-center justify-center rounded-full bg-[var(--margin-note-accent)] text-[10px] leading-none font-semibold text-white tabular-nums transition-shadow ${TIMING.lightClass} dark:text-zinc-950`,\n                        \"size-4 @[34rem]/margin-note:size-2.5\",\n                        lit === note.index\n                          ? \"shadow-[0_0_0_5px_color-mix(in_oklab,var(--margin-note-accent)_28%,transparent)]\"\n                          : \"shadow-[0_0_0_3px_color-mix(in_oklab,var(--margin-note-accent)_20%,transparent)]\",\n                      )}\n                    >\n                      <span className=\"@[34rem]/margin-note:hidden\">{note.index + 1}</span>\n                    </motion.span>\n                  </div>\n                );\n              })}\n\n              {/* the crosshair */}\n              {cross !== null && n > 0 && (\n                <Crosshair\n                  x={geo.main.points[cross].x}\n                  y={geo.main.points[cross].y}\n                  label={labels?.[cross]}\n                  value={format(valueAt(cross))}\n                  compared={compare?.[cross] !== undefined ? format(compare[cross]) : undefined}\n                />\n              )}\n            </div>\n          </div>\n\n          {/* labels along the bottom */}\n          {xLabels.length > 0 && (\n            <div\n              aria-hidden\n              className=\"relative mt-2 h-4 text-[11px] leading-4 text-current/55 tabular-nums\"\n              style={{ marginLeft: gutter }}\n            >\n              {xLabels.map((i) => {\n                const x = geo.main.points[i].x;\n                const edge = x < 0.04 ? \"translate-x-0\" : x > 0.96 ? \"-translate-x-full\" : \"-translate-x-1/2\";\n                return (\n                  <span key={i} className={cn(\"absolute top-0 whitespace-nowrap\", edge)} style={{ left: `${x * 100}%` }}>\n                    {labels?.[i]}\n                  </span>\n                );\n              })}\n            </div>\n          )}\n        </div>\n\n        {/* the margin */}\n        {order.length > 0 && (\n          <ol\n            ref={columnRef}\n            className={cn(\"relative flex flex-col gap-2.5\", placed && \"block\")}\n            style={placed ? { height: placed.column!.h } : undefined}\n          >\n            {order.map((note) => (\n              <NoteItem\n                key={note.index}\n                note={note}\n                shown={note.index < shown && (!still || layout !== null)}\n                still={still}\n                dimmed={lit !== null && lit !== note.index}\n                lit={lit === note.index}\n                top={placed ? placed.tops[note.index] : null}\n                onLight={(on) => setPinned(on ? note.index : null)}\n                onToggle={() => setPinned((p) => (p === note.index ? null : note.index))}\n              />\n            ))}\n          </ol>\n        )}\n\n        {/* leaders, dotted, from each point to its note */}\n        {placed && (\n          <svg aria-hidden className=\"pointer-events-none absolute inset-0 h-full w-full overflow-visible\">\n            {order.map((note) => {\n              const p = geo.main.points[note.anchor];\n              const x1 = placed.plotX + p.x * placed.w + 8;\n              const y1 = placed.plotY + p.y * placed.h;\n              const x2 = placed.column!.x - 7;\n              const y2 = placed.column!.y + placed.tops[note.index] + MARGIN.badge;\n              const on = note.index < shown;\n              // A note that stepped aside needs a bend. Run the long stretch at whichever level, the\n              // point's or the note's, crosses the series fewer times; ties keep the point's level.\n              const k = Math.max(0, Math.min(MARGIN.bend, (x2 - x1) / 4));\n              const level = (y: number) => (y - placed.plotY) / placed.h;\n              const early =\n                Math.abs(y2 - y1) >= 1 && crossings(geo.main, p.x, level(y2)) < crossings(geo.main, p.x, level(y1));\n              const d =\n                Math.abs(y2 - y1) < 1\n                  ? `M${round(x1)},${round(y1)}H${round(x2)}`\n                  : early\n                    ? `M${round(x1)},${round(y1)}C${round(x1 + k)},${round(y1)},${round(x1 + k)},${round(y2)},${round(x1 + 2 * k)},${round(y2)}H${round(x2)}`\n                    : `M${round(x1)},${round(y1)}H${round(x2 - 2 * k)}C${round(x2 - k)},${round(y1)},${round(x2 - k)},${round(y2)},${round(x2)},${round(y2)}`;\n              const isLit = lit === note.index;\n              return (\n                <motion.path\n                  key={`leader-${note.index}`}\n                  d={d}\n                  fill=\"none\"\n                  strokeWidth={1.5}\n                  strokeLinecap=\"round\"\n                  strokeDasharray=\"0 4\"\n                  initial={false}\n                  animate={{\n                    clipPath: on ? \"inset(-8px 0% -8px 0%)\" : \"inset(-8px 100% -8px 0%)\",\n                    opacity: lit === null || isLit ? 1 : 0.5,\n                  }}\n                  transition={still ? { duration: 0 } : { clipPath: on ? TIMING.leader : { duration: 0 }, opacity: TIMING.light }}\n                  className={cn(\n                    `transition-[stroke] ${TIMING.lightClass}`,\n                    isLit ? \"stroke-[color:var(--margin-note-accent)]\" : \"stroke-current/30\",\n                  )}\n                />\n              );\n            })}\n          </svg>\n        )}\n      </div>\n\n      {/* for screen readers: the whole chart in words */}\n      <p id={descId} className=\"sr-only\">\n        {n > 0\n          ? `${n} points${labels?.length ? `, ${labels[0]} to ${labels[n - 1]}` : \"\"}, from ${format(Math.min(...data))} to ${format(Math.max(...data))}. `\n          : \"No data. \"}\n        {order.map((note) => `Note ${note.index + 1}, at ${labels?.[note.anchor] ?? `point ${note.anchor + 1}`}: ${note.title}${note.delta ? ` (${note.delta})` : \"\"}. ${note.body ?? \"\"} `)}\n      </p>\n      <p className=\"sr-only\" aria-live=\"polite\">\n        {spoken}\n      </p>\n    </div>\n  );\n}\n\n/** The x of a point in px, or of the edge of its column when a band ends there. */\nfunction bandX(index: number, n: number, w: number, end = false) {\n  if (n < 2) return end ? w : 0;\n  const step = w / (n - 1);\n  return clamp(index * step + (end ? step / 2 : -step / 2), 0, w);\n}\n\n/** How many times the series crosses a level between a point and the right edge. */\nfunction crossings(main: Series, from: number, level: number) {\n  let count = 0;\n  let side = 0;\n  for (let fx = from + 0.01; fx <= 1; fx += 1 / 160) {\n    const next = Math.sign(heightAt(main, fx) - level);\n    if (next !== 0 && side !== 0 && next !== side) count++;\n    if (next !== 0) side = next;\n  }\n  return count;\n}\n\n/* ── Summary ────────────────────────────────────────────── */\n\n// The takeaway, written last: word by word when it's plain text, as one line otherwise.\nfunction Summary({ summary, shown, instant }: { summary: React.ReactNode; shown: boolean; instant: boolean }) {\n  const words = typeof summary === \"string\" ? summary.split(/\\s+/).filter(Boolean) : null;\n  const step = words ? Math.min(TIMING.word, TIMING.wordSpread / Math.max(1, words.length - 1)) : 0;\n  const hidden = { opacity: 0, transform: `translateY(${MARGIN.lift}px)` };\n  const visible = { opacity: 1, transform: \"translateY(0px)\" };\n  return (\n    <p className=\"mb-3 text-[15px]/6 font-medium tracking-[-0.01em] text-balance\">\n      {words\n        ? words.map((word, i) => (\n            <React.Fragment key={i}>\n              <motion.span\n                className=\"inline-block\"\n                initial={false}\n                animate={shown ? visible : hidden}\n                transition={instant ? { duration: 0 } : { ...TIMING.rise, delay: shown ? i * step : 0 }}\n              >\n                {word}\n              </motion.span>{\" \"}\n            </React.Fragment>\n          ))\n        : (\n            <motion.span\n              className=\"inline-block\"\n              initial={false}\n              animate={shown ? visible : hidden}\n              transition={instant ? { duration: 0 } : TIMING.rise}\n            >\n              {summary}\n            </motion.span>\n          )}\n    </p>\n  );\n}\n\n/* ── NoteItem ───────────────────────────────────────────── */\n\n// One note in the margin: its number, title, figure and a line of detail. A button, so it can be lit\n// from the keyboard; level with its point once the margin is measured.\nfunction NoteItem({\n  note,\n  shown,\n  still,\n  dimmed,\n  lit,\n  top,\n  onLight,\n  onToggle,\n}: {\n  note: Note;\n  shown: boolean;\n  still: boolean;\n  dimmed: boolean;\n  lit: boolean;\n  top: number | null;\n  onLight: (on: boolean) => void;\n  onToggle: () => void;\n}) {\n  // A mouse lights the note on hover; a tap or Enter toggles it instead.\n  const pointer = React.useRef(\"\");\n  return (\n    <motion.li\n      initial={false}\n      animate={\n        shown || still\n          ? { opacity: shown ? 1 : 0, transform: \"translateX(0px)\" }\n          : { opacity: 0, transform: `translateX(-${MARGIN.slide}px)` }\n      }\n      // Written by the reader: slides in behind its leader. Arriving read: fades in once placed.\n      transition={!shown ? { duration: 0 } : still ? TIMING.light : TIMING.note}\n      className={cn(top !== null && \"absolute inset-x-0\", !shown && \"pointer-events-none\")}\n      style={top !== null ? { top } : undefined}\n    >\n      <button\n        type=\"button\"\n        aria-pressed={lit}\n        tabIndex={shown ? 0 : -1}\n        onPointerDown={(event) => {\n          pointer.current = event.pointerType;\n        }}\n        onPointerEnter={(event) => {\n          if (event.pointerType === \"mouse\") onLight(true);\n        }}\n        onPointerLeave={(event) => {\n          if (event.pointerType === \"mouse\") onLight(false);\n        }}\n        onFocus={(event) => {\n          if (event.currentTarget.matches(\":focus-visible\")) onLight(true);\n        }}\n        onBlur={() => onLight(false)}\n        onClick={() => {\n          if (pointer.current !== \"mouse\") onToggle();\n          pointer.current = \"\";\n        }}\n        onKeyDown={(event) => {\n          if (event.key !== \"Escape\") return;\n          event.currentTarget.blur();\n          onLight(false);\n        }}\n        className={cn(\n          `group flex w-full items-start gap-2.5 rounded-lg py-0.5 pr-1 text-left outline-offset-2 outline-[color:var(--margin-note-accent)] transition-opacity ${TIMING.lightClass} focus-visible:outline-2`,\n          dimmed ? \"opacity-45\" : \"opacity-100\",\n        )}\n      >\n        <span className=\"mt-px flex size-[18px] shrink-0 items-center justify-center rounded-full bg-[var(--margin-note-accent)] text-[10px] font-semibold text-white tabular-nums dark:text-zinc-950\">\n          {note.index + 1}\n        </span>\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5\">\n            <span className=\"text-[13px]/5 font-medium tracking-[-0.005em]\">{note.title}</span>\n            {note.delta && (\n              <span className=\"rounded-full bg-[color-mix(in_oklab,var(--margin-note-accent)_14%,transparent)] px-1.5 text-[11px]/[18px] font-medium text-[color:var(--margin-note-accent)] tabular-nums\">\n                {note.delta}\n              </span>\n            )}\n          </span>\n          {note.body && <span className=\"mt-0.5 block text-xs/[1.45] text-pretty text-current/60\">{note.body}</span>}\n        </span>\n      </button>\n    </motion.li>\n  );\n}\n\n/* ── Crosshair ──────────────────────────────────────────── */\n\n// A hairline at the nearest point, a dot on the series and its value beside the dot: above it, or\n// under it when the point sits near the top.\nfunction Crosshair({\n  x,\n  y,\n  label,\n  value,\n  compared,\n}: {\n  x: number;\n  y: number;\n  label?: string;\n  value: string;\n  compared?: string;\n}) {\n  const align = x < 0.15 ? \"translate-x-0\" : x > 0.85 ? \"-translate-x-full\" : \"-translate-x-1/2\";\n  const above = y > 0.28;\n  return (\n    <div aria-hidden className=\"pointer-events-none absolute inset-0\">\n      <div className=\"absolute inset-y-0 w-px bg-current/25\" style={{ left: `${x * 100}%` }} />\n      <div\n        className=\"absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-current shadow-[0_0_0_3px_color-mix(in_oklab,currentColor_18%,transparent)]\"\n        style={{ left: `${x * 100}%`, top: `${y * 100}%` }}\n      />\n      <div\n        className={cn(\n          \"absolute flex items-baseline gap-2 rounded-md bg-zinc-950 px-2 py-1 text-[11px] leading-4 whitespace-nowrap text-white shadow-sm tabular-nums dark:bg-white dark:text-zinc-950\",\n          align,\n          above ? \"-translate-y-[calc(100%+12px)]\" : \"translate-y-3\",\n        )}\n        style={{ left: `${x * 100}%`, top: `${y * 100}%` }}\n      >\n        {label && <span className=\"opacity-60\">{label}</span>}\n        <span className=\"font-medium\">{value}</span>\n        {compared && <span className=\"opacity-60\">vs {compared}</span>}\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}