{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "jot",
  "type": "registry:ui",
  "title": "Jot",
  "description": "A handwritten note that draws its own arrow to the thing it's about. It writes itself in when it scrolls into view, keeps the arrow attached as the layout moves, swings it over when you point it somewhere new, and rubs itself out once it has done its job.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "annotation",
    "svg",
    "onboarding"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/jot",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/jot.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  animate,\n  cancelFrame,\n  cubicBezier,\n  frame,\n  useReducedMotion,\n  type AnimationPlaybackControls,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - The pen lives outside React: it reads both boxes in motion's `frame.read` and paints in\n *   `frame.render`, only while something could move a box, and never off screen.\n * - The words are real text shown by a `clip-path` wipe, line by line; the strokes draw by\n *   `strokeDasharray` on `pathLength={1}` paths, and an undrawn one is hidden outright, since a\n *   zero-length dash still paints a round cap.\n * - The pen writes the words, sets off on the shaft before the last word is done, then draws the\n *   head's two strokes; rubbing out runs backward: head, shaft, words.\n * - The tremor is seeded from the words and fades out at both ends, so the line never shimmers\n *   and still starts and lands exactly.\n * - While shown, the target lists the note in `aria-describedby`; reduced motion drops the pen and\n *   fades the note and arrow in and out by opacity.\n */\nconst TIMING = {\n  /** Before the first write, in seconds (the `delay` prop). */\n  delay: 0,\n  /** How fast the pen writes, draws and rubs out, as a multiple of the pace below (`speed`). */\n  speed: 1,\n  // At the default pace; `speed` divides these.\n  /** Writing the words, in seconds per character. */\n  letter: 0.028,\n  /** The shortest and longest the words take, in seconds. */\n  write: [0.35, 0.7],\n  /** Drawing the shaft, in px/s. */\n  pen: 650,\n  /** The shortest and longest the shaft takes, in seconds. */\n  draw: [0.25, 0.5],\n  /** How long before the last word is done the shaft sets off, in seconds. */\n  lead: 0.1,\n  /** Each of the head's two strokes, in seconds. */\n  stroke: 0.1,\n  /** Between the head's two strokes, in seconds. */\n  stagger: 0.04,\n  /** Every stroke eases in and out, like a hand, as a cubic bezier. */\n  hand: [0.65, 0, 0.35, 1],\n  /** Old words wiping out before the new ones write in, in seconds. */\n  unwrite: 0.15,\n  /** Rubbing out, head first, in seconds each. */\n  rub: { head: 0.06, shaft: 0.2, words: 0.16 },\n  /** How long before the part ahead of it is gone each part starts rubbing out, in seconds. */\n  rubOverlap: { shaft: 0.02, words: 0.04 },\n  /** The arrow swinging over to a new target, as a spring (not paced by `speed`). */\n  swing: { type: \"spring\", visualDuration: 0.45, bounce: 0.12 },\n  /** Reduced motion: the note and its arrow fading in, in seconds. */\n  fadeIn: 0.2,\n  /** Reduced motion: fading out, in seconds. */\n  fadeOut: 0.15,\n  /** Reading boxes every frame after anything that could move one, in ms. */\n  watch: 700,\n  /** Reading on after a transition or scroll ends, in ms. */\n  tail: 250,\n  /** After this, a transition that never reported its end is forgotten, in ms. */\n  stale: 4000,\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * JOT STORYBOARD\n *\n *  rest     a few handwritten words in the margin and nothing drawn\n *           yet. The words are real text in your text color, so\n *           screen readers have them from the start\n *  write    once half the note is on screen (or at mount), the pen\n *           writes the words left to right, line by line, at about\n *           28ms a letter (0.35–0.7s in all)\n *  draw     0.1s before the last word is done the pen sets off from\n *           the side of the note facing the target and draws the\n *           arrow to it at ~650px/s (0.25–0.5s), easing in and out\n *           like a hand. It bows by `bend`, curls once midway with\n *           `loop`, strays ±0.8px either side of true (seeded by the\n *           words, so it never shimmers) and stops `gap` px short of\n *           the target's edge\n *  head     the head goes on last: two short strokes, 11px and 9px,\n *           0.1s each and 0.04s apart\n *  attach   whenever something could move a box (a resize, a scroll,\n *           a transition, a re-render) both boxes are read every frame\n *           until things settle, so the arrow stays attached through\n *           reflows, hover lifts and layout animations. When nothing\n *           moves it stops reading, and off screen it sleeps\n *  swing    point `to` somewhere new and the arrow swings over on a\n *           spring (0.45s, a small 0.12 settle) while the words stay.\n *           New words wipe the old ones out (0.15s) and write in\n *  rub      `show={false}` rubs it out in reverse: the head, then the\n *           shaft back into the note, then the words, ~0.35s in all.\n *           `show` again writes it back from wherever it got to\n *  describe while it shows, the target takes the words as its\n *           description (aria-describedby), so a screen reader on the\n *           button hears the note too\n *  reduced  no pen: the note and its arrow fade in (0.2s) and out\n *           (0.15s), and a new target fades the arrow out and back in\n * ───────────────────────────────────────────────────────── */\n\nconst JOT = {\n  wobble: 0.8, // px the line strays either side of true\n  barbs: [11, 9], // px; the head's two strokes\n  spread: [27, 31], // degrees each stroke leans off the shaft\n  inset: 4, // px between the words and where the arrow leaves them\n  shortest: 14, // px; closer than this and no arrow is drawn\n  view: 0.5, // share of the note on screen before it writes\n} as const;\n\nconst handEase = cubicBezier(...TIMING.hand);\n\ntype Side = \"auto\" | \"top\" | \"right\" | \"bottom\" | \"left\";\ntype Point = { x: number; y: number };\ntype Box = { x: number; y: number; w: number; h: number };\n/** Start, bend control and end of the arrow, in the note's own px. */\ntype Route = { a: Point; c: Point; b: Point };\ntype Line = { l: number; r: number; t: number; b: number };\ntype Lines = { list: Line[]; pad: number };\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\nconst lerpPoint = (p: Point, q: Point, t: number) => ({ x: lerp(p.x, q.x, t), y: lerp(p.y, q.y, t) });\nconst lerpRoute = (p: Route, q: Route, t: number): Route => ({\n  a: lerpPoint(p.a, q.a, t),\n  c: lerpPoint(p.c, q.c, t),\n  b: lerpPoint(p.b, q.b, t),\n});\nconst sameRoute = (p: Route | null, q: Route | null) =>\n  !!p &&\n  !!q &&\n  Math.abs(p.a.x - q.a.x) + Math.abs(p.a.y - q.a.y) + Math.abs(p.b.x - q.b.x) + Math.abs(p.b.y - q.b.y) <\n    0.05 &&\n  Math.abs(p.c.x - q.c.x) + Math.abs(p.c.y - q.c.y) < 0.05;\nconst HIDDEN = \"inset(0 100% 0 0)\";\n\n/** The plain text inside a note, for its pace, its wobble and spotting new words. */\nfunction textOf(node: React.ReactNode): string {\n  if (node == null || typeof node === \"boolean\") return \"\";\n  if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n  if (Array.isArray(node)) return node.map(textOf).join(\"\");\n  if (React.isValidElement<{ children?: React.ReactNode }>(node)) return textOf(node.props.children);\n  return \"\";\n}\n\n/** A hand's tremor: two slow waves with phases picked from the words, so it's the same every render. */\nfunction tremor(text: string) {\n  let h = 2166136261;\n  for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);\n  const next = () => {\n    h = Math.imul(h ^ (h >>> 15), 2246822507);\n    h = Math.imul(h ^ (h >>> 13), 3266489909);\n    return ((h ^= h >>> 16) >>> 0) / 4294967296;\n  };\n  const f1 = 1.2 + next() * 0.7;\n  const f2 = 2.8 + next() * 1.1;\n  const p1 = next();\n  const p2 = next();\n  return (t: number) =>\n    JOT.wobble * (0.65 * Math.sin(2 * Math.PI * (f1 * t + p1)) + 0.35 * Math.sin(2 * Math.PI * (f2 * t + p2)));\n}\n\n/** The side of a box that faces a point: where a ray from its center to the point leaves it. */\nfunction facing(box: Box, p: Point): Exclude<Side, \"auto\"> {\n  const dx = p.x - (box.x + box.w / 2);\n  const dy = p.y - (box.y + box.h / 2);\n  if (Math.abs(dx) / Math.max(box.w / 2, 1) >= Math.abs(dy) / Math.max(box.h / 2, 1)) {\n    return dx >= 0 ? \"right\" : \"left\";\n  }\n  return dy >= 0 ? \"bottom\" : \"top\";\n}\n\n/** A point just outside one side of a box, slid along it toward `toward` but kept off the corners. */\nfunction onSide(box: Box, side: Exclude<Side, \"auto\">, toward: Point, out: number): Point {\n  if (side === \"left\" || side === \"right\") {\n    return {\n      x: side === \"left\" ? box.x - out : box.x + box.w + out,\n      y: clamp(toward.y, box.y + box.h * 0.3, box.y + box.h * 0.7),\n    };\n  }\n  return {\n    x: clamp(toward.x, box.x + box.w * 0.2, box.x + box.w * 0.8),\n    y: side === \"top\" ? box.y - out : box.y + box.h + out,\n  };\n}\n\n/** Where the line from `a` to the box's center enters the box grown by `gap`. */\nfunction entry(a: Point, box: Box, gap: number): Point {\n  const lo = { x: box.x - gap, y: box.y - gap };\n  const hi = { x: box.x + box.w + gap, y: box.y + box.h + gap };\n  const d = { x: box.x + box.w / 2 - a.x, y: box.y + box.h / 2 - a.y };\n  let enter = 0;\n  for (const axis of [\"x\", \"y\"] as const) {\n    if (Math.abs(d[axis]) < 1e-6) continue;\n    const t1 = (lo[axis] - a[axis]) / d[axis];\n    const t2 = (hi[axis] - a[axis]) / d[axis];\n    enter = Math.max(enter, Math.min(t1, t2));\n  }\n  enter = clamp(enter, 0, 1);\n  return { x: a.x + d.x * enter, y: a.y + d.y * enter };\n}\n\nfunction plan(note: Box, target: Box, o: { from: Side; at: Side; bend: number; gap: number }): Route | null {\n  const center = { x: target.x + target.w / 2, y: target.y + target.h / 2 };\n  const a = onSide(note, o.from === \"auto\" ? facing(note, center) : o.from, center, JOT.inset);\n  const b = o.at === \"auto\" ? entry(a, target, o.gap) : onSide(target, o.at, a, o.gap);\n  const dx = b.x - a.x;\n  const dy = b.y - a.y;\n  const d = Math.hypot(dx, dy);\n  if (d < JOT.shortest) return null;\n  // Bow to the right of travel for a positive bend (clockwise on screen), by a share of the length.\n  const bend = clamp(o.bend, -1, 1);\n  return {\n    a,\n    b,\n    c: { x: (a.x + b.x) / 2 - (dy / d) * bend * d * 0.5, y: (a.y + b.y) / 2 + (dx / d) * bend * d * 0.5 },\n  };\n}\n\n/** The shaft as a sampled line with the tremor and the optional curl, plus the tip and its heading. */\nfunction trace(r: Route, loop: boolean, bend: number, shake: (t: number) => number) {\n  const { a, c, b } = r;\n  const chord = Math.hypot(b.x - a.x, b.y - a.y);\n  const length = (Math.hypot(c.x - a.x, c.y - a.y) + Math.hypot(b.x - c.x, b.y - c.y) + chord) / 2;\n  const radius = clamp(chord * 0.08, 8, 14);\n  const curl = loop && length > radius * 7;\n  // The curl takes a stretch of the line about 2.6 radii long, centred halfway along.\n  const span = curl ? clamp((2.6 * radius) / length, 0.08, 0.3) : 0;\n  const t0 = 0.5 - span / 2;\n  const ts: number[] = [];\n  const step = Math.max(3 / Math.max(length, 1), 1 / 160);\n  for (let t = 0; t < 1; t += step) if (!curl || t <= t0 || t >= t0 + span) ts.push(t);\n  if (curl) for (let i = 1; i < 40; i++) ts.push(t0 + (span * i) / 40);\n  ts.push(1);\n  ts.sort((p, q) => p - q);\n  const side = bend < 0 ? -1 : 1;\n  let d = \"\";\n  for (const t of ts) {\n    const u = 1 - t;\n    let x = u * u * a.x + 2 * u * t * c.x + t * t * b.x;\n    let y = u * u * a.y + 2 * u * t * c.y + t * t * b.y;\n    let tx = 2 * u * (c.x - a.x) + 2 * t * (b.x - c.x);\n    let ty = 2 * u * (c.y - a.y) + 2 * t * (b.y - c.y);\n    const tl = Math.hypot(tx, ty) || 1;\n    tx /= tl;\n    ty /= tl;\n    // The tremor fades out at both ends, so the line still starts and lands exactly.\n    const wobble = shake(t) * Math.sin(Math.PI * t);\n    x += -ty * wobble;\n    y += tx * wobble;\n    if (curl && t > t0 && t < t0 + span) {\n      // A circle rolled along the line: forward at its foot, back over the top, forward again.\n      const th = ((t - t0) / span) * Math.PI * 2;\n      x += radius * (tx * Math.sin(th) - side * ty * (1 - Math.cos(th)));\n      y += radius * (ty * Math.sin(th) + side * tx * (1 - Math.cos(th)));\n    }\n    d += `${d ? \"L\" : \"M\"}${x.toFixed(1)} ${y.toFixed(1)}`;\n  }\n  let hx = b.x - c.x;\n  let hy = b.y - c.y;\n  const hl = Math.hypot(hx, hy);\n  if (hl < 0.01) {\n    hx = b.x - a.x;\n    hy = b.y - a.y;\n  }\n  const n = Math.hypot(hx, hy) || 1;\n  return { d, tip: b, heading: { x: hx / n, y: hy / n } };\n}\n\nfunction barb(tip: Point, heading: Point, length: number, degrees: number) {\n  const rad = (degrees * Math.PI) / 180;\n  const bx = -heading.x;\n  const by = -heading.y;\n  const x = tip.x + (bx * Math.cos(rad) - by * Math.sin(rad)) * length;\n  const y = tip.y + (bx * Math.sin(rad) + by * Math.cos(rad)) * length;\n  return `M${tip.x.toFixed(1)} ${tip.y.toFixed(1)}L${x.toFixed(1)} ${y.toFixed(1)}`;\n}\n\n/** The words' line boxes in their own px, measured upright even when the note is tilted. */\nfunction measureLines(words: HTMLElement, turner: HTMLElement | null, scale: number): Lines | null {\n  const turned = turner?.style.rotate ?? \"\";\n  if (turner) turner.style.rotate = \"0deg\";\n  const box = words.getBoundingClientRect();\n  const range = document.createRange();\n  range.selectNodeContents(words);\n  const rects = Array.from(range.getClientRects());\n  if (turner) turner.style.rotate = turned;\n  if (!box.width || !box.height) return null;\n  const list: Line[] = [];\n  for (const rect of rects) {\n    if (rect.width < 0.5 || rect.height < 0.5) continue;\n    const l = (rect.left - box.left) / scale;\n    const r = (rect.right - box.left) / scale;\n    const t = (rect.top - box.top) / scale;\n    const b = (rect.bottom - box.top) / scale;\n    const mid = (t + b) / 2;\n    const line = list.find((x) => mid > x.t && mid < x.b);\n    if (line) {\n      line.l = Math.min(line.l, l);\n      line.r = Math.max(line.r, r);\n      line.t = Math.min(line.t, t);\n      line.b = Math.max(line.b, b);\n    } else list.push({ l, r, t, b });\n  }\n  list.sort((p, q) => p.t - q.t);\n  const size = parseFloat(getComputedStyle(words).fontSize) || 16;\n  return { list, pad: size * 0.4 };\n}\n\n/** A clip that shows the words up to where the pen has got: whole lines above, part of the current one. */\nfunction wipe(lines: Lines | null, progress: number) {\n  if (progress >= 1) return \"none\";\n  if (progress <= 0 || !lines || !lines.list.length) return HIDDEN;\n  const { list, pad } = lines;\n  const px = (n: number) => `${n.toFixed(1)}px`;\n  const left = px(Math.min(...list.map((x) => x.l)) - pad);\n  const right = px(Math.max(...list.map((x) => x.r)) + pad);\n  const top = px(list[0].t - pad);\n  // Lines meet halfway between one's bottom and the next one's top; the last runs out to the padding.\n  const below = (i: number) => px(i === list.length - 1 ? list[i].b + pad : (list[i].b + list[i + 1].t) / 2);\n  let ink = progress * list.reduce((sum, x) => sum + (x.r - x.l), 0);\n  for (let i = 0; i < list.length; i++) {\n    const w = list[i].r - list[i].l;\n    if (ink <= w || i === list.length - 1) {\n      // The pen sweeps each line from just before its first letter to just past its last.\n      const pen = px(list[i].l - pad + (Math.min(ink, w) / Math.max(w, 1)) * (w + 2 * pad));\n      if (i === 0) return `polygon(${left} ${top}, ${pen} ${top}, ${pen} ${below(0)}, ${left} ${below(0)})`;\n      return `polygon(${left} ${top}, ${right} ${top}, ${right} ${below(i - 1)}, ${pen} ${below(i - 1)}, ${pen} ${below(i)}, ${left} ${below(i)})`;\n    }\n    ink -= w;\n  }\n  return \"none\";\n}\n\nfunction addToken(el: Element, attr: string, token: string) {\n  const list = (el.getAttribute(attr) ?? \"\").split(/\\s+/).filter(Boolean);\n  if (!list.includes(token)) el.setAttribute(attr, [...list, token].join(\" \"));\n}\n\nfunction removeToken(el: Element, attr: string, token: string) {\n  const list = (el.getAttribute(attr) ?? \"\").split(/\\s+/).filter((t) => t && t !== token);\n  if (list.length) el.setAttribute(attr, list.join(\" \"));\n  else el.removeAttribute(attr);\n}\n\n/* ── Pen ──────────────────────────────────────────────── */\n\n// Everything that moves lives here, outside React. Each note reads both boxes in motion's read\n// step and paints in its render step, so many notes on one page never force extra layouts.\n\ntype Els = {\n  root: HTMLElement;\n  turner: HTMLElement;\n  words: HTMLElement;\n  svg: SVGSVGElement;\n  shaft: SVGPathElement;\n  barbs: [SVGPathElement, SVGPathElement];\n};\n\ntype PenOptions = {\n  to?: React.RefObject<Element | null> | string;\n  from: Side;\n  at: Side;\n  bend: number;\n  loop: boolean;\n  gap: number;\n  trigger: \"view\" | \"mount\";\n  delay: number;\n  speed: number;\n  show: boolean;\n  describe: boolean;\n  reduced: boolean;\n  text: string;\n};\n\ntype Key = \"words\" | \"shaft\" | \"head\" | \"swing\" | \"ghost\" | \"fadeWords\" | \"fadeArrow\";\ntype Run = { controls: AnimationPlaybackControls; to: number; end: number };\n\nconst HEAD = TIMING.stroke + TIMING.stagger; // s for both strokes of the head\n\nfunction createPen(els: Els, id: string) {\n  let o: PenOptions | null = null;\n  // words / shaft / head: how much the pen has put down, 0–1. swing: from the old route to the\n  // new one (springs a little past 1). ghost: old words still showing. fade*: reduced motion.\n  const v: Record<Key, number> = { words: 0, shaft: 0, head: 0, swing: 1, ghost: 0, fadeWords: 1, fadeArrow: 1 };\n  const runs: Partial<Record<Key, Run>> = {};\n  let triggered = false;\n  let wrote = false;\n  let near = false; // on or near the screen\n  let looping = false; // reading and painting every frame\n  let until = 0; // ms; keep reading until then, in case something is still moving\n  let watched: Element | null = null; // the target the resize observer is on\n  // CSS transitions and animations running in the scope: element → property → when it last stirred.\n  const moving = new Map<EventTarget, Map<string, number>>();\n  let target: Element | null = null;\n  let described: Element | null = null;\n  let live: Route | null = null;\n  let drawn: Route | null = null;\n  let swingFrom: Route | null = null;\n  let lines: Lines | null = null;\n  let linesDirty = true;\n  let ghost: { el: HTMLElement; lines: Lines | null } | null = null;\n  let width = 0;\n  let shake = tremor(\"\");\n  let painted = \"\";\n  const observers: { disconnect(): void }[] = [];\n\n  const now = () => performance.now() / 1000;\n\n  // `speed` scales every stroke of the pen, writing and rubbing out alike.\n  const pace = () => 1 / Math.max(0.05, o?.speed ?? TIMING.speed);\n  const writeTime = () => clamp((o?.text.length ?? 0) * TIMING.letter, TIMING.write[0], TIMING.write[1]) * pace();\n  const drawTime = () => {\n    if (!live) return TIMING.draw[0] * pace();\n    const { a, c, b } = live;\n    const length = (Math.hypot(c.x - a.x, c.y - a.y) + Math.hypot(b.x - c.x, b.y - c.y) + Math.hypot(b.x - a.x, b.y - a.y)) / 2;\n    return clamp(length / TIMING.pen, TIMING.draw[0], TIMING.draw[1]) * pace();\n  };\n\n  const stop = (key: Key) => {\n    runs[key]?.controls.stop();\n    delete runs[key];\n  };\n\n  /** Tween one value and return when it lands. A run already heading to the same place is kept. */\n  const tween = (key: Key, to: number, duration: number, delay: number, ease: \"hand\" | \"linear\" = \"hand\", then?: () => void) => {\n    const run = runs[key];\n    if (run && run.to === to && !then) return run.end;\n    stop(key);\n    if (Math.abs(v[key] - to) < 1e-4) {\n      v[key] = to;\n      then?.();\n      return now();\n    }\n    const wait = Math.max(delay, 0);\n    const controls = animate(v[key], to, {\n      duration: Math.max(duration, 0.001),\n      delay: wait,\n      ease: ease === \"hand\" ? handEase : (t: number) => t,\n      onUpdate: (value) => {\n        v[key] = value;\n      },\n      onComplete: () => {\n        v[key] = to;\n        if (runs[key]?.controls === controls) delete runs[key];\n        then?.();\n      },\n    });\n    const end = now() + wait + duration;\n    runs[key] = { controls, to, end };\n    poke(0);\n    return end;\n  };\n\n  /** Bring every value to where it should be, in pen order: words, shaft, head, or the reverse. */\n  const sync = () => {\n    if (!o) return;\n    const wantWords = triggered && o.show;\n    const wantArrow = wantWords && !!live;\n\n    if (o.reduced) {\n      // No pen: what's there is all there, and opacity carries the change.\n      stop(\"words\");\n      stop(\"shaft\");\n      stop(\"head\");\n      if (wantWords) {\n        if (v.words < 1) {\n          v.words = 1;\n          v.fadeWords = 0;\n        }\n        tween(\"fadeWords\", 1, TIMING.fadeIn * (1 - v.fadeWords), 0, \"linear\");\n      } else tween(\"fadeWords\", 0, TIMING.fadeOut * v.fadeWords, 0, \"linear\");\n      if (wantArrow) {\n        if (v.shaft < 1 || v.head < 1) {\n          v.shaft = 1;\n          v.head = 1;\n          v.fadeArrow = 0;\n        }\n        tween(\"fadeArrow\", 1, TIMING.fadeIn * (1 - v.fadeArrow), 0, \"linear\");\n      } else tween(\"fadeArrow\", 0, TIMING.fadeOut * v.fadeArrow, 0, \"linear\");\n      return;\n    }\n\n    stop(\"fadeWords\");\n    stop(\"fadeArrow\");\n    v.fadeWords = 1;\n    v.fadeArrow = 1;\n    const t = now();\n\n    if (wantWords) {\n      const delay = wrote ? 0 : o.delay;\n      wrote = true;\n      const wordsEnd = tween(\"words\", 1, (1 - v.words) * writeTime(), delay);\n      if (wantArrow) {\n        const start = Math.max(t, wordsEnd - TIMING.lead * pace());\n        const shaftEnd = tween(\"shaft\", 1, (1 - v.shaft) * drawTime(), start - t);\n        tween(\"head\", 1, (1 - v.head) * HEAD * pace(), shaftEnd - t, \"linear\");\n      } else rubArrow();\n      return;\n    }\n\n    const shaftEnd = rubArrow();\n    tween(\"words\", 0, v.words * TIMING.rub.words * pace(), Math.max(0, shaftEnd - t - TIMING.rubOverlap.words * pace()));\n  };\n\n  /** Head first, then the shaft back into the note. Returns when the shaft is gone. */\n  const rubArrow = () => {\n    const t = now();\n    const headEnd = tween(\"head\", 0, v.head * TIMING.rub.head * pace(), 0, \"linear\");\n    return tween(\"shaft\", 0, v.shaft * TIMING.rub.shaft * pace(), Math.max(0, headEnd - t - TIMING.rubOverlap.shaft * pace()));\n  };\n\n  /** Swing from whatever is drawn now to the live route, if anything is drawn yet. */\n  const startSwing = () => {\n    if (!drawn || v.shaft <= 0) return;\n    swingFrom = drawn;\n    stop(\"swing\");\n    v.swing = 0;\n    if (o?.reduced) {\n      // Fade the arrow out where it is, then back in at its new place.\n      tween(\"fadeArrow\", 0, TIMING.fadeOut * v.fadeArrow, 0, \"linear\", () => {\n        v.swing = 1;\n        swingFrom = null;\n        tween(\"fadeArrow\", 1, TIMING.fadeIn, 0, \"linear\");\n      });\n      return;\n    }\n    const controls = animate(0, 1, {\n      ...TIMING.swing,\n      onUpdate: (value) => {\n        v.swing = value;\n      },\n      onComplete: () => {\n        v.swing = 1;\n        swingFrom = null;\n        if (runs.swing?.controls === controls) delete runs.swing;\n      },\n    });\n    runs.swing = { controls, to: 1, end: Infinity };\n    poke(0);\n  };\n\n  const redescribe = () => {\n    const want = o && o.describe && o.show ? target : null;\n    if (want === described) return;\n    if (described) removeToken(described, \"aria-describedby\", id);\n    described = want;\n    if (want) addToken(want, \"aria-describedby\", id);\n  };\n\n  const resolve = (): Element | null => {\n    const to = o?.to;\n    if (!to) return null;\n    if (typeof to !== \"string\") return to.current;\n    const scope = els.root.closest(\"[data-jot-scope]\") ?? document;\n    try {\n      return scope.querySelector(to);\n    } catch {\n      return null;\n    }\n  };\n\n  const read = () => {\n    if (!o) return;\n    const { root, turner, words } = els;\n    const rootRect = root.getBoundingClientRect();\n    if (!rootRect.width && !rootRect.height) return;\n    // A scaled ancestor shrinks every rect alike; dividing it out keeps the arrow in the note's own px.\n    const scale = root.offsetWidth ? rootRect.width / root.offsetWidth || 1 : 1;\n    if (linesDirty) {\n      lines = measureLines(words, turner, scale);\n      linesDirty = false;\n    }\n    width = words.offsetWidth;\n\n    const next = resolve();\n    const hadRoute = !!live;\n    if (next !== target) {\n      if (target && next) startSwing();\n      target = next;\n      redescribe();\n    }\n    if (target !== watched) {\n      if (watched) resize.unobserve(watched);\n      watched = target;\n      if (watched) resize.observe(watched);\n    }\n    let route: Route | null = null;\n    const tr = target?.isConnected ? target.getBoundingClientRect() : null;\n    if (tr && (tr.width || tr.height)) {\n      const local = (r: DOMRect): Box => ({\n        x: (r.left - rootRect.left) / scale,\n        y: (r.top - rootRect.top) / scale,\n        w: r.width / scale,\n        h: r.height / scale,\n      });\n      route = plan(local(turner.getBoundingClientRect()), local(tr), o);\n    }\n    live = route;\n    // The target turned up, went away or came back: draw the arrow or rub it out.\n    if (hadRoute !== !!route) sync();\n  };\n\n  const paint = (force = false) => {\n    if (!o) return;\n    const { words, svg, shaft, barbs } = els;\n    let route = live ?? drawn;\n    if (live && swingFrom) route = lerpRoute(swingFrom, live, v.swing);\n    const head1 = handEase(clamp((v.head * HEAD) / TIMING.stroke, 0, 1));\n    const head2 = handEase(clamp((v.head * HEAD - TIMING.stagger) / TIMING.stroke, 0, 1));\n    // Exact values: a rounded key would hide the last step to 1, and the words would stop a hair short.\n    const key = [v.words, v.shaft, head1, head2, v.ghost, v.fadeWords, v.fadeArrow, o.loop, lines?.list.length].join();\n    if (!force && key === painted && sameRoute(route, drawn)) return;\n    painted = key;\n\n    words.style.clipPath = wipe(lines, v.words);\n    words.style.opacity = v.fadeWords < 1 ? String(v.fadeWords) : \"\";\n    if (ghost) {\n      ghost.el.style.clipPath = wipe(ghost.lines, v.ghost);\n      ghost.el.style.visibility = v.ghost > 0 ? \"visible\" : \"hidden\";\n    }\n\n    svg.style.opacity = v.fadeArrow < 1 ? String(v.fadeArrow) : \"\";\n    if (!route) {\n      for (const p of [shaft, ...barbs]) p.style.visibility = \"hidden\";\n      return;\n    }\n    drawn = route;\n    const line = trace(route, o.loop, o.bend, shake);\n    shaft.setAttribute(\"d\", line.d);\n    barbs[0].setAttribute(\"d\", barb(line.tip, line.heading, JOT.barbs[0], JOT.spread[0]));\n    barbs[1].setAttribute(\"d\", barb(line.tip, line.heading, JOT.barbs[1], -JOT.spread[1]));\n    const dash = (p: SVGPathElement, progress: number) => {\n      // A zero-length dash still paints a round cap, so an undrawn stroke is hidden outright.\n      p.style.visibility = progress > 0.001 ? \"visible\" : \"hidden\";\n      p.style.strokeDasharray = progress >= 1 ? \"\" : `${progress} 1`;\n    };\n    dash(shaft, v.shaft);\n    dash(barbs[0], head1);\n    dash(barbs[1], head2);\n  };\n\n  const loop = (on: boolean) => {\n    if (on === looping) return;\n    looping = on;\n    if (on) {\n      frame.read(read, true);\n      frame.render(render, true);\n    } else {\n      cancelFrame(read);\n      cancelFrame(render);\n    }\n  };\n\n  /** Read and paint every frame for a while, because something may be moving. */\n  const poke = (ms: number = TIMING.watch) => {\n    until = Math.max(until, performance.now() + ms);\n    if (near) loop(true);\n  };\n\n  const render = () => {\n    paint();\n    const t = performance.now();\n    moving.forEach((props, el) => {\n      props.forEach((at, name) => {\n        if (t - at > TIMING.stale) props.delete(name);\n      });\n      if (!props.size) moving.delete(el);\n    });\n    // Settled: nothing of ours is animating, nothing in the scope is moving, nothing stirred lately.\n    if (!Object.keys(runs).length && !moving.size && t > until) loop(false);\n  };\n\n  const arm = () => {\n    if (!o || triggered) return;\n    if (o.trigger === \"mount\") {\n      triggered = true;\n      sync();\n      return;\n    }\n    const view = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry?.isIntersecting) return;\n        view.disconnect();\n        triggered = true;\n        sync();\n      },\n      { threshold: JOT.view },\n    );\n    view.observe(els.root);\n    observers.push(view);\n  };\n\n  // Near the screen it watches for anything that could move a box; off screen it does nothing.\n  const scope = els.root.closest(\"[data-jot-scope]\") ?? document.body;\n  const nearby = new IntersectionObserver(\n    ([entry]) => {\n      near = !!entry?.isIntersecting;\n      if (near) poke();\n      else loop(false);\n    },\n    { rootMargin: \"200px\" },\n  );\n  nearby.observe(els.root);\n  const resize = new ResizeObserver((entries) => {\n    if (entries.some((e) => e.target === els.words)) linesDirty = true;\n    poke();\n  });\n  resize.observe(els.words);\n  resize.observe(els.root);\n  if (els.root.parentElement) resize.observe(els.root.parentElement);\n  // Our own paint writes styles inside the note; only changes elsewhere count.\n  const mutations = new MutationObserver((records) => {\n    if (records.some((r) => !els.root.contains(r.target))) poke(TIMING.watch / 2);\n  });\n  // Every attribute, so a selector like `[data-active]` finds its new match the moment it moves.\n  mutations.observe(scope, { subtree: true, childList: true, characterData: true, attributes: true });\n  observers.push(nearby, resize, mutations);\n  const stir = (e: Event) => {\n    const el = e.target;\n    const relevant =\n      !(el instanceof Node) || scope.contains(el) || el.contains(els.root) || (!!target && el.contains(target));\n    if (!el || !relevant) return;\n    if (e instanceof TransitionEvent || e instanceof AnimationEvent) {\n      const name = e instanceof TransitionEvent ? e.propertyName : e.animationName;\n      const props = moving.get(el) ?? new Map<string, number>();\n      if (e.type === \"transitionend\" || e.type === \"transitioncancel\" || e.type === \"animationend\" || e.type === \"animationcancel\") {\n        props.delete(name);\n      } else props.set(name, performance.now());\n      if (props.size) moving.set(el, props);\n      else moving.delete(el);\n    }\n    poke(TIMING.tail);\n  };\n  const events = [\n    \"scroll\",\n    \"transitionrun\",\n    \"transitionend\",\n    \"transitioncancel\",\n    \"animationstart\",\n    \"animationiteration\",\n    \"animationend\",\n    \"animationcancel\",\n  ];\n  events.forEach((type) => document.addEventListener(type, stir, { capture: true, passive: true }));\n  const onResize = () => poke();\n  window.addEventListener(\"resize\", onResize, { passive: true });\n  document.fonts?.ready.then(() => {\n    linesDirty = true;\n    poke();\n  });\n\n  return {\n    set(next: PenOptions) {\n      const prev = o;\n      o = next;\n      poke();\n      if (!prev || prev.text !== next.text) shake = tremor(next.text);\n      if (!prev || prev.trigger !== next.trigger) arm();\n      if (prev && (prev.from !== next.from || prev.at !== next.at || prev.bend !== next.bend || prev.gap !== next.gap)) {\n        startSwing();\n      }\n      if (!prev || prev.show !== next.show || prev.reduced !== next.reduced) sync();\n      if (!prev || prev.describe !== next.describe || prev.show !== next.show) redescribe();\n    },\n\n    /** New words: the old ones wipe out from a copy, then the new ones write in. */\n    rewrite(el: HTMLElement) {\n      if (!o || v.words <= 0) return;\n      const { root } = els;\n      const scale = root.offsetWidth ? root.getBoundingClientRect().width / root.offsetWidth || 1 : 1;\n      el.style.width = width ? `${width}px` : \"max-content\";\n      ghost = { el, lines: measureLines(el, els.turner, scale) };\n      linesDirty = true;\n      stop(\"words\");\n      v.ghost = v.words;\n      v.words = 0;\n      const writing = triggered && o.show;\n      if (o.reduced) {\n        v.ghost = 0;\n        v.words = writing ? 1 : 0;\n      } else {\n        const out = TIMING.unwrite * v.ghost * pace();\n        tween(\"ghost\", 0, out, 0, \"linear\");\n        if (writing) tween(\"words\", 1, writeTime(), out);\n      }\n      startSwing();\n      paint(true);\n    },\n\n    destroy() {\n      loop(false);\n      observers.forEach((ob) => ob.disconnect());\n      events.forEach((type) => document.removeEventListener(type, stir, { capture: true }));\n      window.removeEventListener(\"resize\", onResize);\n      (Object.keys(runs) as Key[]).forEach(stop);\n      if (described) removeToken(described, \"aria-describedby\", id);\n      described = null;\n      o = null;\n    },\n  };\n}\n\ntype Pen = ReturnType<typeof createPen>;\n\n/* ── Jot ──────────────────────────────────────────────── */\n\nexport interface JotProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** The words. Keep them short: a few words on one or two lines. */\n  children: React.ReactNode;\n  /**\n   * What the arrow points at: a ref, or a CSS selector looked up inside the nearest\n   * `[data-jot-scope]` ancestor (the whole document without one). Leave it out for a note\n   * with no arrow.\n   */\n  to?: React.RefObject<Element | null> | string;\n  /** The side of the note the arrow leaves from. `\"auto\"` picks the side facing the target. */\n  from?: Side;\n  /** The side of the target the arrow lands on. `\"auto\"` aims at its center and stops at its edge. */\n  at?: Side;\n  /** How far the arrow bows, from -1 to 1. 0 is nearly straight; flip the sign to bow the other way. */\n  bend?: number;\n  /** Curl the arrow once midway, like a quick flourish. */\n  loop?: boolean;\n  /** Pixels left between the arrowhead and the target. */\n  gap?: number;\n  /** Degrees to turn the words, for a looser hand. The arrow isn't turned. */\n  tilt?: number;\n  /** When it writes itself: once half of it is on screen, or as soon as it mounts. */\n  trigger?: \"view\" | \"mount\";\n  /** Seconds to wait before the first write, to write several notes one after another. */\n  delay?: number;\n  /** How fast the pen writes, draws and rubs out, as a multiple of the default pace. `2` is twice as fast. */\n  speed?: number;\n  /** Set to false to rub the note out; true writes it back in. */\n  show?: boolean;\n  /** Describe the target with the note's words for screen readers, through `aria-describedby`. */\n  describe?: boolean;\n}\n\nexport function Jot({\n  children,\n  to,\n  from = \"auto\",\n  at = \"auto\",\n  bend = 0.35,\n  loop = false,\n  gap = 6,\n  tilt = 0,\n  trigger = \"view\",\n  delay = TIMING.delay,\n  speed = TIMING.speed,\n  show = true,\n  describe = true,\n  className,\n  ...props\n}: JotProps) {\n  const reduced = useReducedMotion() ?? false;\n  const id = React.useId();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const turnerRef = React.useRef<HTMLSpanElement>(null);\n  const wordsRef = React.useRef<HTMLSpanElement>(null);\n  const ghostRef = React.useRef<HTMLSpanElement>(null);\n  const svgRef = React.useRef<SVGSVGElement>(null);\n  const shaftRef = React.useRef<SVGPathElement>(null);\n  const barbARef = React.useRef<SVGPathElement>(null);\n  const barbBRef = React.useRef<SVGPathElement>(null);\n  const penRef = React.useRef<Pen | null>(null);\n\n  // New words keep the old ones on screen as a copy, so they can wipe out before the new ones write.\n  const text = textOf(children);\n  const [kept, setKept] = React.useState({ text, children });\n  const [ghost, setGhost] = React.useState<{ key: number; children: React.ReactNode } | null>(null);\n  if (kept.text !== text) {\n    setGhost({ key: (ghost?.key ?? 0) + 1, children: kept.children });\n    setKept({ text, children });\n  }\n\n  React.useLayoutEffect(() => {\n    const [root, turner, words, svg, shaft, a, b] = [\n      rootRef.current,\n      turnerRef.current,\n      wordsRef.current,\n      svgRef.current,\n      shaftRef.current,\n      barbARef.current,\n      barbBRef.current,\n    ];\n    if (!root || !turner || !words || !svg || !shaft || !a || !b) return;\n    const pen = createPen({ root, turner, words, svg, shaft, barbs: [a, b] }, id);\n    penRef.current = pen;\n    return () => {\n      pen.destroy();\n      penRef.current = null;\n    };\n  }, [id]);\n\n  React.useLayoutEffect(() => {\n    penRef.current?.set({ to, from, at, bend, loop, gap, trigger, delay, speed, show, describe, reduced, text });\n  });\n\n  React.useLayoutEffect(() => {\n    if (ghost && ghostRef.current) penRef.current?.rewrite(ghostRef.current);\n  }, [ghost]);\n\n  return (\n    <div\n      ref={rootRef}\n      data-jot=\"\"\n      aria-hidden={show ? undefined : true}\n      className={cn(\n        \"pointer-events-none relative z-10 w-max max-w-56 font-['Segoe_Print','Bradley_Hand',Chilanka,casual,cursive] text-lg/snug select-none\",\n        className,\n      )}\n      {...props}\n    >\n      <span ref={turnerRef} className=\"relative block\" style={tilt ? { rotate: `${tilt}deg` } : undefined}>\n        <span ref={wordsRef} id={id} className=\"block\" style={{ clipPath: HIDDEN }}>\n          {children}\n        </span>\n        {ghost ? (\n          <span\n            key={ghost.key}\n            ref={ghostRef}\n            aria-hidden\n            className=\"absolute top-0 left-0 block\"\n            style={{ clipPath: HIDDEN, visibility: \"hidden\" }}\n          >\n            {ghost.children}\n          </span>\n        ) : null}\n      </span>\n      <svg\n        ref={svgRef}\n        aria-hidden\n        className=\"pointer-events-none absolute top-0 left-0 size-px overflow-visible\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.75}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      >\n        <path ref={shaftRef} pathLength={1} style={{ visibility: \"hidden\" }} />\n        <path ref={barbARef} pathLength={1} style={{ visibility: \"hidden\" }} />\n        <path ref={barbBRef} pathLength={1} style={{ visibility: \"hidden\" }} />\n      </svg>\n    </div>\n  );\n}\n"
    }
  ]
}