{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "redline",
  "type": "registry:ui",
  "title": "Redline",
  "description": "Figma-style redlines for any layout. Point at an element to see its size, padding and gaps, with lines and labels that glide from one element to the next. Pin one and hold Alt to measure the distance to another.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/redline.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * REDLINE STORYBOARD\n *\n *  point    once the pointer settles on an element, a 1px box\n *           with corner handles lands on it, its size in a pill\n *           underneath. Padding is hatched and labeled per side,\n *           gaps between flex and grid children are hatched and\n *           labeled once, and red lines measure the space to the\n *           nearest neighbor or the parent's edge\n *  glide    pointing at another element carries all of it there\n *           on one quick spring: the box resizes on the way, the\n *           lines and hatches follow, and their numbers tick to\n *           the new values. A line that wasn't there before draws\n *           outward from the edge it measures. Sweeping across\n *           nested elements doesn't flicker: the box moves once\n *           the pointer slows or settles\n *  text     with spec, a card beside text gives its font, size\n *           and line height, weight, letter spacing and color\n *  pin      a click pins the element, instead of pressing it.\n *           Hold Alt (Option) and point at another to measure\n *           between the two; when they don't overlap on an axis,\n *           dashed guides carry its edges out to the lines\n *  leave    everything fades out quickly, unless something is\n *           pinned\n *  tour     with nobody pointing, it walks a list of elements on\n *           its own while on screen. The pointer coming in stops\n *           it, and it picks up again 2s after the pointer leaves\n *  keys     ↑ parent, ↓ first child, ←/→ siblings. Enter pins;\n *           moving on then measures between the two. Esc clears\n *  touch    a tap selects, a tap on another element measures\n *           between them, a tap outside clears\n *  reduced  no glides or drawing: everything moves at once\n * ───────────────────────────────────────────────────────── */\n\nconst REDLINE = {\n  glide: { type: \"spring\", visualDuration: 0.2, bounce: 0 },\n  draw: { duration: 0.3, ease: [0.22, 1, 0.36, 1] }, // a line drawing outward the first time\n  fade: { duration: 0.12, ease: \"easeOut\" },\n  dwell: 90, // ms the pointer rests on an element before the box moves to it\n  slow: 0.3, // px/ms; a pointer moving slower than this moves the box at once\n  step: 2500, // ms per tour stop\n  resume: 2000, // ms after the pointer leaves before the tour picks up again\n  pill: 24, // px a size pill needs below or above its box\n  card: 12, // px between a box and the spec card beside it\n} as const;\n\nconst COLORS = {\n  selection: \"#0C8CE9\",\n  distance: \"#F24822\",\n  spacing: \"#C026D3\",\n};\n\nconst PILL =\n  \"rounded-[3px] px-1 py-px text-[11px]/[14px] font-medium whitespace-nowrap text-white tabular-nums\";\n\nconst HATCH =\n  \"repeating-linear-gradient(-45deg, color-mix(in srgb, var(--redline-space) 34%, transparent) 0 1px, transparent 1px 5px), linear-gradient(color-mix(in srgb, var(--redline-space) 6%, transparent), color-mix(in srgb, var(--redline-space) 6%, transparent))\";\n\n// Each line is revealed from the edge it measures, outward.\nconst DRAW_FROM = {\n  top: \"inset(100% 0% 0% 0%)\",\n  right: \"inset(0% 100% 0% 0%)\",\n  bottom: \"inset(0% 0% 100% 0%)\",\n  left: \"inset(0% 0% 0% 100%)\",\n} as const;\nconst DRAWN = \"inset(0% 0% 0% 0%)\";\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\nconst SIDES: Side[] = [\"top\", \"right\", \"bottom\", \"left\"];\n\ntype Rect = { x: number; y: number; w: number; h: number };\n/** A hatched area: padding or a gap. `axis` is the dimension its value measures. */\ntype Strip = {\n  key: string;\n  rect: Rect;\n  value: number;\n  axis: \"x\" | \"y\";\n  label: boolean;\n  /** A column gap too narrow for its label puts it above. */\n  above?: boolean;\n};\n/**\n * A distance line, starting at (x, y) and running `length` down (top, bottom) or across (left, right).\n * `span` is the range it can slide along to keep clear of other labels; `edge` means it ends at the\n * parent's edge. Its label sits beside it (`side`), on the other side (`flip`), or past its outer end.\n */\ntype Line = {\n  key: string;\n  side: Side;\n  x: number;\n  y: number;\n  length: number;\n  span: [number, number];\n  edge: boolean;\n  tag: \"side\" | \"flip\" | \"end\";\n};\ntype Guide = { key: string; axis: \"x\" | \"y\"; x: number; y: number; length: number };\n\ntype Scene = {\n  main: Rect;\n  other: Rect | null;\n  strips: Strip[];\n  lines: Line[];\n  guides: Guide[];\n  text: RedlineText | null;\n  pill: \"below\" | \"above\" | \"inside\";\n  /** Where the text spec card goes, and how far from the box. */\n  card: { side: \"right\" | \"left\" | \"below\"; offset: number } | null;\n};\n\ntype Point = { el: Element; via: \"mouse\" | \"key\" | \"touch\" | \"tour\"; to?: Element | null };\n\ntype Tokens = Record<number, string>;\n\n/** The font of a text element, as `RedlineSelection.text` reports it. */\nexport interface RedlineText {\n  /** The first family in the font stack, e.g. \"Inter\". */\n  font: string;\n  size: number;\n  /** In px, or null for `line-height: normal`. */\n  lineHeight: number | null;\n  weight: number;\n  /** As a percentage of the font size, the way Figma shows it: -0.16px on 16px text is -1. */\n  letterSpacing: number;\n  /** Hex, with an alpha pair when it isn't opaque. */\n  color: string;\n}\n\n/** What `onSelect` reports about the selected element. Sizes are CSS pixels. */\nexport interface RedlineSelection {\n  element: Element;\n  /** The element's `data-redline` name, if it has one. */\n  name: string | null;\n  width: number;\n  height: number;\n  padding: Record<Side, number>;\n  /** The row and column gap of a flex or grid container, otherwise null. */\n  gap: { row: number; column: number } | null;\n  /** Space to the nearest neighbor, or to the parent's edge, on each side that has any. */\n  spacing: Partial<Record<Side, number>>;\n  /** The font, for elements with text of their own; otherwise null. */\n  text: RedlineText | null;\n}\n\n/** A tour stop: one element's `data-redline` name, or two names to measure between. */\nexport type RedlineTourStep = string | [string, string];\n\n/* ── Formatting ─────────────────────────────────────────── */\n\n/** Whole pixels, or one decimal when the layout isn't on whole pixels. */\nconst px = (value: number) => {\n  const rounded = Math.round(value * 10) / 10;\n  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);\n};\n\nconst named = (value: number, tokens?: Tokens) => tokens?.[Number(px(value))] ?? px(value);\n\nconst percent = (value: number) => {\n  const rounded = Math.round(value * 10) / 10;\n  return rounded === 0 ? \"0%\" : `${rounded < 0 ? \"−\" : \"\"}${Math.abs(rounded)}%`;\n};\n\nlet paint: CanvasRenderingContext2D | null | undefined;\nconst hexes = new Map<string, string>();\n\n/** Any CSS color (Tailwind's oklch included) as hex. */\nfunction hexOf(color: string) {\n  const known = hexes.get(color);\n  if (known) return known;\n  paint ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!paint) return color;\n  paint.clearRect(0, 0, 1, 1);\n  paint.fillStyle = color;\n  paint.fillRect(0, 0, 1, 1);\n  const [r, g, b, a] = paint.getImageData(0, 0, 1, 1).data;\n  const pair = (n: number) => n.toString(16).padStart(2, \"0\");\n  const hex = `#${pair(r)}${pair(g)}${pair(b)}${a < 255 ? pair(a) : \"\"}`.toUpperCase();\n  hexes.set(color, hex);\n  return hex;\n}\n\nfunction fontOf(family: string) {\n  const first = family.split(\",\")[0].trim().replace(/^[\"']|[\"']$/g, \"\");\n  const hashed = /^__(.+?)_[\\da-f]{5,}$/i.exec(first); // next/font's \"__Inter_a1b2c3\"\n  return (hashed ? hashed[1].replace(/_/g, \" \") : first) || \"sans-serif\";\n}\n\nconst specOf = (text: RedlineText) => [\n  text.font,\n  `${px(text.size)}/${text.lineHeight == null ? \"Auto\" : px(text.lineHeight)}`,\n  String(text.weight),\n  percent(text.letterSpacing),\n  text.color,\n];\n\nfunction nameOf(el: Element) {\n  const label =\n    el.getAttribute(\"data-redline\") ?? el.getAttribute(\"aria-label\") ?? el.getAttribute(\"alt\");\n  if (label) return label;\n  const text = el.textContent?.trim().replace(/\\s+/g, \" \");\n  if (text) return text.length > 32 ? `${text.slice(0, 31)}…` : text;\n  return el.tagName.toLowerCase();\n}\n\n/* ── Measuring ──────────────────────────────────────────── */\n\nconst right = (r: Rect) => r.x + r.w;\nconst bottom = (r: Rect) => r.y + r.h;\nconst overlap = (a0: number, a1: number, b0: number, b1: number) =>\n  Math.min(a1, b1) - Math.max(a0, b0);\n\n/**\n * The root's padding box in the viewport, so rects can be measured in its own coordinates.\n * `clips` when it scrolls or hides overflow, so labels have to stay inside it.\n */\ntype Frame = {\n  left: number;\n  top: number;\n  scale: number;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  clips: boolean;\n};\n\nfunction frameOf(root: HTMLElement): Frame {\n  const r = root.getBoundingClientRect();\n  const scale = root.offsetWidth > 0 ? r.width / root.offsetWidth || 1 : 1;\n  return {\n    left: r.left + root.clientLeft * scale,\n    top: r.top + root.clientTop * scale,\n    scale,\n    x: root.scrollLeft,\n    y: root.scrollTop,\n    width: root.clientWidth,\n    height: root.clientHeight,\n    clips: getComputedStyle(root).overflow !== \"visible\",\n  };\n}\n\nfunction rectOf(frame: Frame, el: Element): Rect {\n  const r = el.getBoundingClientRect();\n  return {\n    x: (r.left - frame.left) / frame.scale + frame.x,\n    y: (r.top - frame.top) / frame.scale + frame.y,\n    w: r.width / frame.scale,\n    h: r.height / frame.scale,\n  };\n}\n\nconst sides = (cs: CSSStyleDeclaration, name: (side: Side) => string) =>\n  SIDES.map((side) => parseFloat(cs.getPropertyValue(name(side))) || 0) as [\n    number,\n    number,\n    number,\n    number,\n  ];\n\nconst inset = (r: Rect, [t, ri, b, l]: number[]): Rect => ({\n  x: r.x + l,\n  y: r.y + t,\n  w: Math.max(0, r.w - l - ri),\n  h: Math.max(0, r.h - t - b),\n});\n\n/** Whether an element can be measured: inside the root, matching the selector, and not empty. */\nfunction isPart(el: Element, root: HTMLElement, selector: string) {\n  if (el === root || el.closest(\"[data-redline-ui]\") || !el.matches(selector)) return false;\n  const r = el.getBoundingClientRect();\n  return r.width > 0 || r.height > 0;\n}\n\n/** The measurable element at a pointer target. An icon counts as one piece, not its paths. */\nfunction partAt(target: EventTarget | null, root: HTMLElement, selector: string) {\n  let el = target instanceof Element ? target : null;\n  if (el instanceof SVGElement) {\n    let svg = el instanceof SVGSVGElement ? el : el.ownerSVGElement;\n    while (svg?.ownerSVGElement) svg = svg.ownerSVGElement;\n    el = svg ?? el;\n  }\n  for (; el && el !== root && root.contains(el); el = el.parentElement) {\n    if (isPart(el, root, selector)) return el;\n  }\n  return null;\n}\n\nfunction parentPart(el: Element, root: HTMLElement, selector: string) {\n  for (let p = el.parentElement; p && p !== root; p = p.parentElement) {\n    if (isPart(p, root, selector)) return p;\n  }\n  return null;\n}\n\n/** The nearest measurable descendants: through wrappers that don't match, never into an icon. */\nfunction childParts(of: Element, root: HTMLElement, selector: string) {\n  const parts: Element[] = [];\n  const walk = (node: Element) => {\n    for (const child of Array.from(node.children)) {\n      if (child.hasAttribute(\"data-redline-ui\")) continue;\n      if (isPart(child, root, selector)) parts.push(child);\n      else if (!(child instanceof SVGElement)) walk(child);\n    }\n  };\n  walk(of);\n  return parts;\n}\n\n/** A flex container's items, in order, without absolutely positioned or hidden ones. */\nfunction flexItems(el: Element): Element[] {\n  return Array.from(el.children).flatMap((child) => {\n    if (child.hasAttribute(\"data-redline-ui\")) return [];\n    const cs = getComputedStyle(child);\n    if (cs.display === \"contents\") return flexItems(child);\n    if (cs.display === \"none\" || cs.position === \"absolute\" || cs.position === \"fixed\") return [];\n    return [child];\n  });\n}\n\nfunction gapsOf(frame: Frame, el: Element, cs: CSSStyleDeclaration, content: Rect): Strip[] {\n  if (cs.display.endsWith(\"grid\")) {\n    const strips: Strip[] = [];\n    const lanes = (tracks: string, gap: number, start: number, room: number, align: string, axis: \"x\" | \"y\") => {\n      const sizes = Array.from(tracks.matchAll(/(-?[\\d.]+)px/g), (m) => parseFloat(m[1]));\n      if (sizes.length < 2 || !(gap >= 0.5)) return;\n      const total = sizes.reduce((sum, size) => sum + size, 0) + gap * (sizes.length - 1);\n      let at = start + (align.includes(\"center\") ? (room - total) / 2 : align.includes(\"end\") ? room - total : 0);\n      for (let i = 0; i < sizes.length - 1; i++) {\n        at += sizes[i];\n        strips.push({\n          key: `${axis === \"x\" ? \"col\" : \"row\"}-${i}`,\n          rect:\n            axis === \"x\"\n              ? { x: at, y: content.y, w: gap, h: content.h }\n              : { x: content.x, y: at, w: content.w, h: gap },\n          value: gap,\n          axis,\n          label: i === 0,\n        });\n        at += gap;\n      }\n    };\n    lanes(cs.gridTemplateColumns, parseFloat(cs.columnGap), content.x, content.w, cs.justifyContent, \"x\");\n    lanes(cs.gridTemplateRows, parseFloat(cs.rowGap), content.y, content.h, cs.alignContent, \"y\");\n    return strips;\n  }\n\n  if (!cs.display.endsWith(\"flex\")) return [];\n  const row = cs.flexDirection.startsWith(\"row\");\n  const wraps = cs.flexWrap !== \"nowrap\";\n  const items = flexItems(el)\n    .map((item) => rectOf(frame, item))\n    .filter((r) => r.w > 0 || r.h > 0);\n  const strips: Strip[] = [];\n  for (let i = 1; i < items.length; i++) {\n    const a = items[i - 1];\n    const b = items[i];\n    // Main axis start and end, then cross axis start and end.\n    const span = (r: Rect) =>\n      row ? [r.x, right(r), r.y, bottom(r)] : [r.y, bottom(r), r.x, right(r)];\n    const [a0, a1, ac0, ac1] = span(a);\n    const [b0, b1, bc0, bc1] = span(b);\n    if (overlap(ac0, ac1, bc0, bc1) <= 0) continue; // a new flex line\n    const [from, to] = b0 >= a1 - 0.5 ? [a1, b0] : a0 >= b1 - 0.5 ? [b1, a0] : [0, 0];\n    if (to - from < 0.5) continue;\n    const c0 = wraps ? Math.min(ac0, bc0) : row ? content.y : content.x;\n    const c1 = wraps ? Math.max(ac1, bc1) : row ? bottom(content) : right(content);\n    strips.push({\n      key: `gap-${strips.length}`,\n      rect: row ? { x: from, y: c0, w: to - from, h: c1 - c0 } : { x: c0, y: from, w: c1 - c0, h: to - from },\n      value: to - from,\n      axis: row ? \"x\" : \"y\",\n      label: true,\n    });\n  }\n  // Equal gaps are labeled once.\n  const even = strips.every((s) => Math.abs(s.value - strips[0].value) < 0.05);\n  return strips.map((s, i) => ({ ...s, label: !even || i === 0 }));\n}\n\n/** Space from each side of a box to its nearest neighbor across from it, or else the parent's edge. */\nfunction spaceAround(box: Rect, siblings: Rect[], within: Rect): Line[] {\n  const lines: Line[] = [];\n  for (const side of SIDES) {\n    const vertical = side === \"top\" || side === \"bottom\";\n    const before = side === \"top\" || side === \"left\";\n    const lo = (r: Rect) => (vertical ? r.y : r.x);\n    const hi = (r: Rect) => (vertical ? bottom(r) : right(r));\n    const across = (r: Rect): [number, number] => (vertical ? [r.x, right(r)] : [r.y, bottom(r)]);\n    const [b0, b1] = across(box);\n    let edge = before ? lo(within) : hi(within);\n    let span: [number, number] = [b0, b1];\n    let neighbor = false;\n    for (const s of siblings) {\n      const [s0, s1] = across(s);\n      const shared: [number, number] = [Math.max(b0, s0), Math.min(b1, s1)];\n      if (shared[1] - shared[0] < 0.5) continue;\n      const nearer = before\n        ? hi(s) <= lo(box) + 0.5 && hi(s) > edge\n        : lo(s) >= hi(box) - 0.5 && lo(s) < edge;\n      if (!nearer) continue;\n      edge = before ? hi(s) : lo(s);\n      span = shared;\n      neighbor = true;\n    }\n    const from = before ? edge : hi(box);\n    const length = before ? lo(box) - edge : edge - hi(box);\n    if (length < 0.5) continue;\n    const at = (span[0] + span[1]) / 2;\n    lines.push({\n      key: side,\n      side,\n      x: vertical ? at : from,\n      y: vertical ? from : at,\n      length,\n      span,\n      edge: !neighbor,\n      tag: \"side\",\n    });\n  }\n  return lines;\n}\n\n/** Figma's Alt measurement from a (pinned) to b. Guides are added once the lines are placed. */\nfunction between(a: Rect, b: Rect): Line[] {\n  const lines: Line[] = [];\n  const ox = overlap(a.x, right(a), b.x, right(b));\n  const oy = overlap(a.y, bottom(a), b.y, bottom(b));\n  // Lines run through the shared range, or from a's middle when there's none.\n  const spanX: [number, number] = ox > 0 ? [Math.max(a.x, b.x), Math.min(right(a), right(b))] : [a.x, right(a)];\n  const spanY: [number, number] = oy > 0 ? [Math.max(a.y, b.y), Math.min(bottom(a), bottom(b))] : [a.y, bottom(a)];\n  const add = (side: Side, from: number, to: number) => {\n    const length = to - from;\n    if (length < 0.5) return;\n    const vertical = side === \"top\" || side === \"bottom\";\n    const span = vertical ? spanX : spanY;\n    const at = (span[0] + span[1]) / 2;\n    lines.push({\n      key: `m-${side}`,\n      side,\n      x: vertical ? at : from,\n      y: vertical ? from : at,\n      length,\n      span,\n      edge: false,\n      tag: \"side\",\n    });\n  };\n\n  if (ox > 0 && oy > 0) {\n    // Overlapping or nested: the space between matching edges.\n    add(\"left\", Math.min(a.x, b.x), Math.max(a.x, b.x));\n    add(\"right\", Math.min(right(a), right(b)), Math.max(right(a), right(b)));\n    add(\"top\", Math.min(a.y, b.y), Math.max(a.y, b.y));\n    add(\"bottom\", Math.min(bottom(a), bottom(b)), Math.max(bottom(a), bottom(b)));\n    return lines;\n  }\n  if (b.x >= right(a)) add(\"right\", right(a), b.x);\n  else if (right(b) <= a.x) add(\"left\", right(b), a.x);\n  if (b.y >= bottom(a)) add(\"bottom\", bottom(a), b.y);\n  else if (bottom(b) <= a.y) add(\"top\", bottom(b), a.y);\n  return lines;\n}\n\n/** Where a line misses b on the other axis, a dashed guide carries b's edge out to it. */\nfunction guidesTo(b: Rect, lines: Line[]): Guide[] {\n  return lines.flatMap((line): Guide[] => {\n    const vertical = line.side === \"top\" || line.side === \"bottom\";\n    const at = vertical ? line.x : line.y;\n    const [b0, b1] = vertical ? [b.x, right(b)] : [b.y, bottom(b)];\n    if (at >= b0 - 0.5 && at <= b1 + 0.5) return [];\n    const end = line.side === \"right\" || line.side === \"bottom\" ? (vertical ? line.y : line.x) + line.length : vertical ? line.y : line.x;\n    const near = at < b0 ? b0 : b1;\n    const from = Math.min(at, near);\n    const length = Math.abs(near - at);\n    return [\n      vertical\n        ? { key: `g-${line.side}`, axis: \"x\", x: from, y: end, length }\n        : { key: `g-${line.side}`, axis: \"y\", x: end, y: from, length },\n    ];\n  });\n}\n\nconst labelWidth = (text: string) => text.length * 6.5 + 8;\n\n/**\n * Keep labels off each other: a line slides along its span to clear the padding label on its\n * side and the size pill, and a short line ending at the parent's edge puts its label past it.\n */\nfunction placeLines(lines: Line[], box: Rect, strips: Strip[], pill: Scene[\"pill\"], tokens?: Tokens) {\n  const pillHalf = labelWidth(`${px(box.w)} × ${px(box.h)}`) / 2 + 6;\n  return lines.map((line): Line => {\n    const vertical = line.side === \"top\" || line.side === \"bottom\";\n    const center = vertical ? box.x + box.w / 2 : box.y + box.h / 2;\n    let keep = strips.some((strip) => strip.key === `pad-${line.side}` && strip.label)\n      ? vertical\n        ? 16\n        : 12\n      : 0;\n    if ((line.side === \"bottom\" && pill === \"below\") || (line.side === \"top\" && pill === \"above\")) {\n      keep = Math.max(keep, pillHalf);\n    }\n    let at = vertical ? line.x : line.y;\n    let tag: Line[\"tag\"] = \"side\";\n    const [s0, s1] = line.span;\n    if (keep && Math.abs(at - center) < keep) {\n      if (center + keep <= s1 - 1) at = center + keep;\n      else if (center - keep >= s0 + 1) {\n        at = center - keep;\n        tag = \"flip\";\n      }\n    }\n    if (!vertical && line.edge && line.length < labelWidth(named(line.length, tokens)) + 12) tag = \"end\";\n    return vertical ? { ...line, x: at, tag } : { ...line, y: at, tag };\n  });\n}\n\nfunction textOf(el: Element, cs: CSSStyleDeclaration): RedlineText | null {\n  const own = Array.from(el.childNodes).some(\n    (node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),\n  );\n  if (!own) return null;\n  const size = parseFloat(cs.fontSize) || 0;\n  const tracking = cs.letterSpacing === \"normal\" ? 0 : parseFloat(cs.letterSpacing) || 0;\n  return {\n    font: fontOf(cs.fontFamily),\n    size,\n    lineHeight: cs.lineHeight === \"normal\" ? null : parseFloat(cs.lineHeight),\n    weight: Number(cs.fontWeight) || 400,\n    letterSpacing: size ? (tracking / size) * 100 : 0,\n    color: hexOf(cs.color),\n  };\n}\n\n/** Everything about one element: its box, padding, gaps and the space around it. */\nfunction inspect(root: HTMLElement, frame: Frame, el: Element, selector: string) {\n  const box = rectOf(frame, el);\n  const cs = getComputedStyle(el);\n  const border = sides(cs, (side) => `border-${side}-width`);\n  const [pt, pr, pb, pl] = sides(cs, (side) => `padding-${side}`);\n  const inner = inset(box, border);\n  const content = inset(inner, [pt, pr, pb, pl]);\n\n  // Top and bottom padding run the full width; left and right fit between them.\n  const pads: [Side, Rect, number][] = [\n    [\"top\", { ...inner, h: pt }, pt],\n    [\"right\", { x: right(inner) - pr, y: inner.y + pt, w: pr, h: inner.h - pt - pb }, pr],\n    [\"bottom\", { ...inner, y: bottom(inner) - pb, h: pb }, pb],\n    [\"left\", { x: inner.x, y: inner.y + pt, w: pl, h: inner.h - pt - pb }, pl],\n  ];\n  const strips: Strip[] = pads\n    .filter(([, rect, value]) => value >= 0.5 && rect.w > 0 && rect.h > 0)\n    .map(([side, rect, value]) => ({\n      key: `pad-${side}`,\n      rect,\n      value,\n      axis: side === \"top\" || side === \"bottom\" ? \"y\" : \"x\",\n      label: true,\n    }));\n  strips.push(...gapsOf(frame, el, cs, content));\n\n  const parent = parentPart(el, root, selector);\n  const siblings = childParts(parent ?? root, root, selector)\n    .filter((sibling) => sibling !== el)\n    .map((sibling) => rectOf(frame, sibling));\n  const within = parent\n    ? inset(rectOf(frame, parent), sides(getComputedStyle(parent), (side) => `border-${side}-width`))\n    : { x: frame.x, y: frame.y, w: frame.width, h: frame.height }; // the root's visible padding box\n\n  const flowing = cs.display.endsWith(\"flex\") || cs.display.endsWith(\"grid\");\n  return {\n    box,\n    parent,\n    strips,\n    lines: spaceAround(box, siblings, within),\n    padding: { top: pt, right: pr, bottom: pb, left: pl },\n    gap: flowing ? { row: parseFloat(cs.rowGap) || 0, column: parseFloat(cs.columnGap) || 0 } : null,\n    text: textOf(el, cs),\n  };\n}\n\ntype Look = ReturnType<typeof inspect>;\n\nfunction sceneOf(\n  frame: Frame,\n  look: Look,\n  other: Rect | null,\n  measuring: boolean,\n  spec: boolean,\n  tokens?: Tokens,\n): Scene {\n  const box = look.box;\n  const view = { x: frame.x, y: frame.y, r: frame.x + frame.width, b: frame.y + frame.height };\n  const pill =\n    !frame.clips || bottom(box) + REDLINE.pill <= view.b\n      ? \"below\"\n      : box.y - REDLINE.pill >= view.y\n        ? \"above\"\n        : \"inside\";\n  if (other && measuring) {\n    const lines = placeLines(between(box, other), box, [], pill, tokens);\n    return { main: box, other, strips: [], lines, guides: guidesTo(other, lines), text: null, pill, card: null };\n  }\n\n  const strips = look.strips.map((strip) =>\n    strip.axis === \"x\" && !strip.key.startsWith(\"pad\") && labelWidth(named(strip.value, tokens)) > strip.rect.w + 4\n      ? { ...strip, above: true }\n      : strip,\n  );\n  const lines = placeLines(look.lines, box, strips, pill, tokens);\n  // The spec card goes beside the box, past the distance line on that side, or else under it.\n  const text = spec ? look.text : null;\n  let card: Scene[\"card\"] = null;\n  if (text) {\n    const width = labelWidth(specOf(text).join(\" · \")) + 32;\n    const past = (side: Side) => {\n      const line = lines.find((l) => l.side === side);\n      if (!line) return REDLINE.card;\n      return line.length + (line.tag === \"end\" ? labelWidth(named(line.length, tokens)) + 6 : 0) + REDLINE.card;\n    };\n    const r = past(\"right\");\n    const l = past(\"left\");\n    card =\n      view.r - right(box) - r >= width\n        ? { side: \"right\", offset: r }\n        : box.x - view.x - l >= width\n          ? { side: \"left\", offset: l }\n          : { side: \"below\", offset: 0 };\n  }\n  return { main: box, other, strips, lines, guides: [], text, pill, card };\n}\n\nconst round = (value: number) => Math.round(value * 10) / 10;\n\nfunction selectionOf(el: Element, look: Look): RedlineSelection {\n  return {\n    element: el,\n    name: el.getAttribute(\"data-redline\"),\n    width: round(look.box.w),\n    height: round(look.box.h),\n    padding: look.padding,\n    gap: look.gap,\n    spacing: Object.fromEntries(look.lines.map((line) => [line.side, round(line.length)])),\n    text: look.text,\n  };\n}\n\n/* ── Redline ────────────────────────────────────────────── */\n\nexport interface RedlineColors {\n  /** The selection box, its handles and size pill. */\n  selection?: string;\n  /** Distance lines, guides and their labels. */\n  distance?: string;\n  /** Padding and gap hatching and their labels. */\n  spacing?: string;\n}\n\nexport interface RedlineProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n  /** Elements to walk through on their own while nobody is pointing, by `data-redline` name. A pair of names measures between the two. */\n  tour?: RedlineTourStep[];\n  /** Names for pixel values, so spacing labels read \"space-4\" instead of \"16\". */\n  tokens?: Tokens;\n  /** Show a card beside text elements with their font, size, line height, weight, letter spacing and color. */\n  spec?: boolean;\n  /** Which elements can be measured. Any element inside by default. */\n  selector?: string;\n  /** Called when the selected element changes, with its measurements, or with null when nothing is selected. */\n  onSelect?: (selection: RedlineSelection | null) => void;\n  /** Overrides for the selection, distance and spacing colors. */\n  colors?: RedlineColors;\n}\n\ntype Live = {\n  point: Point | null;\n  pinned: Element | null;\n  alt: boolean;\n  inside: boolean;\n  focused: boolean;\n  /** The element last reported through onSelect. */\n  shown: Element | null;\n  scene: string;\n  observer: ResizeObserver | null;\n  observed: Element[];\n  pointer: { x: number; y: number; t: number; speed: number; timer: number; kind: string };\n  tour: { step: number; inView: boolean; idle: boolean; timer: number; resume: number };\n};\n\n/**\n * Figma-style redlines for any layout. Point at an element to see its size,\n * padding and gaps; pin one and hold Alt to measure the distance to another.\n */\nexport function Redline({\n  tour,\n  tokens,\n  spec = false,\n  selector = \"*\",\n  onSelect,\n  colors,\n  className,\n  style,\n  children,\n  \"aria-label\": ariaLabel = \"Measurements\",\n  onPointerEnter,\n  onPointerMove,\n  onPointerLeave,\n  onPointerDown,\n  onMouseDownCapture,\n  onClickCapture,\n  onKeyDown,\n  onFocus,\n  onBlur,\n  ...props\n}: RedlineProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const reduced = useReducedMotion() ?? false;\n  const [scene, setScene] = React.useState<(Scene & { instant: boolean }) | null>(null);\n  const [message, setMessage] = React.useState(\"\");\n  const hintId = React.useId();\n\n  const latest = React.useRef({ tour, spec, selector, tokens, onSelect });\n  React.useLayoutEffect(() => {\n    latest.current = { tour, spec, selector, tokens, onSelect };\n  });\n\n  const live = React.useRef<Live>({\n    point: null,\n    pinned: null,\n    alt: false,\n    inside: false,\n    focused: false,\n    shown: null,\n    scene: \"null\",\n    observer: null,\n    observed: [],\n    pointer: { x: NaN, y: NaN, t: 0, speed: Infinity, timer: 0, kind: \"mouse\" },\n    tour: { step: 0, inView: false, idle: true, timer: 0, resume: 0 },\n  });\n\n  /** Measure what's pointed at and redraw. `instant` skips the glide, for scrolling and resizing. */\n  const refresh = (instant = false) => {\n    const root = rootRef.current;\n    if (!root) return;\n    const s = live.current;\n    const { spec, selector, tokens, onSelect } = latest.current;\n    const alive = (el: Element | null | undefined) => (el?.isConnected && root.contains(el) ? el : null);\n    s.pinned = alive(s.pinned);\n    if (s.point && !alive(s.point.el)) s.point = null;\n\n    // What gets the box, and what it's measured against.\n    const p = s.point;\n    let main = p?.el ?? null;\n    let other = p?.via === \"tour\" ? alive(p.to) : null;\n    let measuring = !!other;\n    if (s.pinned) {\n      main = s.pinned;\n      other = p && p.el !== s.pinned ? p.el : null;\n      measuring = !!other && (p?.via !== \"mouse\" || s.alt);\n    }\n\n    let next: Scene | null = null;\n    let look: Look | null = null;\n    if (main) {\n      const frame = frameOf(root);\n      look = inspect(root, frame, main, selector);\n      next = sceneOf(frame, look, other && rectOf(frame, other), measuring, spec, tokens);\n    }\n    if (main !== s.shown) {\n      s.shown = main;\n      onSelect?.(main && look ? selectionOf(main, look) : null);\n    }\n    const json = JSON.stringify(next);\n    if (json !== s.scene) {\n      s.scene = json;\n      setScene(next && { ...next, instant });\n    }\n\n    // Follow the size of everything the scene depends on.\n    const watch = [root, main, look?.parent, other].filter((el): el is Element => !!el);\n    const moved = watch.length !== s.observed.length || watch.some((el, i) => el !== s.observed[i]);\n    if (s.observer && moved) {\n      s.observer.disconnect();\n      for (const el of watch) s.observer.observe(el);\n      s.observed = watch;\n    }\n  };\n\n  const describe = (el: Element) => {\n    const root = rootRef.current;\n    if (!root) return \"\";\n    const s = live.current;\n    const frame = frameOf(root);\n    const r = rectOf(frame, el);\n    let text = `${nameOf(el)}, ${px(r.w)} by ${px(r.h)}`;\n    if (s.pinned && s.pinned !== el) {\n      const lines = between(rectOf(frame, s.pinned), r);\n      const parts = lines.map((line) => `${line.side} ${px(line.length)}`).join(\", \");\n      if (parts) text += `. From ${nameOf(s.pinned)}: ${parts}`;\n    }\n    return text;\n  };\n\n  const announce = (text: string) =>\n    setMessage((last) => (last === text ? `${text} ` : text)); // repeat the same words too\n\n  /* Tour: steps on its own while on screen and nobody's using it. */\n  const syncTour = () => {\n    const s = live.current;\n    const t = s.tour;\n    const steps = latest.current.tour ?? [];\n    const running = steps.length > 0 && t.inView && t.idle && !s.pinned && !s.inside && !s.focused;\n    if (!running) {\n      window.clearTimeout(t.timer);\n      t.timer = 0;\n      return;\n    }\n    if (t.timer) return;\n    const root = rootRef.current;\n    if (!root) return;\n    const find = (name: string) => root.querySelector(`[data-redline=\"${CSS.escape(name)}\"]`);\n    for (let i = 0; i < steps.length; i++) {\n      const index = (t.step + i) % steps.length;\n      const [first, second] = Array.isArray(steps[index]) ? steps[index] : [steps[index] as string, null];\n      const el = find(first);\n      if (!el) continue;\n      t.step = index;\n      s.point = { el, via: \"tour\", to: second ? find(second) : null };\n      break;\n    }\n    refresh();\n    t.timer = window.setTimeout(() => {\n      t.timer = 0;\n      t.step += 1;\n      actions.current.syncTour();\n    }, REDLINE.step);\n  };\n\n  /** Someone's using it: the tour stops where it is. */\n  const engage = () => {\n    const t = live.current.tour;\n    t.idle = false;\n    window.clearTimeout(t.resume);\n    syncTour();\n  };\n\n  /** Nobody is: the tour picks up again after a moment. */\n  const release = () => {\n    const s = live.current;\n    const t = s.tour;\n    window.clearTimeout(t.resume);\n    if (s.inside || s.focused || s.pinned) return;\n    t.resume = window.setTimeout(() => {\n      t.idle = true;\n      if (s.point?.via !== \"tour\") t.step += 1;\n      actions.current.syncTour();\n    }, REDLINE.resume);\n  };\n\n  const clear = () => {\n    const s = live.current;\n    s.pinned = null;\n    s.point = null;\n    refresh();\n    release();\n  };\n\n  // Global listeners, observers and timers call the latest versions of these.\n  const actions = React.useRef({ refresh, syncTour, clear });\n  React.useLayoutEffect(() => {\n    actions.current = { refresh, syncTour, clear };\n  });\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const s = live.current;\n    let frame = 0;\n    const remeasure = () => {\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(() => actions.current.refresh(true));\n    };\n    const observer = new ResizeObserver(remeasure);\n    observer.observe(root);\n    s.observer = observer;\n    s.observed = [root];\n    root.addEventListener(\"scroll\", remeasure, { capture: true, passive: true });\n    window.addEventListener(\"resize\", remeasure);\n    document.fonts?.ready.then(remeasure);\n\n    const view = new IntersectionObserver(([entry]) => {\n      s.tour.inView = entry.isIntersecting;\n      actions.current.syncTour();\n    });\n    view.observe(root);\n\n    // A tap outside clears a touch selection.\n    const outside = (event: PointerEvent) => {\n      if (event.pointerType === \"mouse\" || root.contains(event.target as Node)) return;\n      if (s.pinned || s.point?.via === \"touch\") actions.current.clear();\n    };\n    document.addEventListener(\"pointerdown\", outside, true);\n\n    const setAlt = (alt: boolean) => {\n      if (s.alt === alt) return;\n      s.alt = alt;\n      if (s.pinned && s.point?.via === \"mouse\") actions.current.refresh();\n    };\n    const keyDown = (event: KeyboardEvent) => event.key === \"Alt\" && setAlt(true);\n    const keyUp = (event: KeyboardEvent) => {\n      if (event.key !== \"Alt\") return;\n      setAlt(false);\n      if (s.inside) event.preventDefault(); // keep Windows from focusing the menu bar\n    };\n    const blur = () => setAlt(false);\n    window.addEventListener(\"keydown\", keyDown);\n    window.addEventListener(\"keyup\", keyUp);\n    window.addEventListener(\"blur\", blur);\n\n    return () => {\n      cancelAnimationFrame(frame);\n      observer.disconnect();\n      s.observer = null;\n      view.disconnect();\n      root.removeEventListener(\"scroll\", remeasure, { capture: true });\n      window.removeEventListener(\"resize\", remeasure);\n      document.removeEventListener(\"pointerdown\", outside, true);\n      window.removeEventListener(\"keydown\", keyDown);\n      window.removeEventListener(\"keyup\", keyUp);\n      window.removeEventListener(\"blur\", blur);\n      window.clearTimeout(s.pointer.timer);\n      window.clearTimeout(s.tour.timer);\n      window.clearTimeout(s.tour.resume);\n      s.tour.timer = 0;\n    };\n  }, []);\n\n  // New props: remeasure, and start, restart or drop the tour.\n  const tourKey = JSON.stringify(tour ?? []);\n  const tokensKey = JSON.stringify(tokens ?? {});\n  React.useLayoutEffect(() => {\n    const s = live.current;\n    if (!JSON.parse(tourKey).length && s.point?.via === \"tour\") s.point = null;\n    window.clearTimeout(s.tour.timer);\n    s.tour.timer = 0;\n    actions.current.syncTour();\n    actions.current.refresh(true);\n  }, [tourKey, tokensKey, spec, selector]);\n\n  /* Pointer: the box follows the mouse once it settles. Taps are handled on click. */\n  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    onPointerMove?.(event);\n    const root = rootRef.current;\n    if (event.pointerType !== \"mouse\" || !root) return;\n    const s = live.current;\n    const p = s.pointer;\n    if (s.alt !== event.altKey) {\n      s.alt = event.altKey;\n      if (s.pinned) refresh();\n    }\n    const dx = event.clientX - p.x;\n    const dy = event.clientY - p.y;\n    if (dx === 0 && dy === 0) return;\n    const dt = event.timeStamp - p.t;\n    const speed = Number.isNaN(dx) ? Infinity : dt > 0 ? Math.hypot(dx, dy) / dt : p.speed;\n    p.speed = Number.isFinite(p.speed) && Number.isFinite(speed) ? (p.speed + speed) / 2 : speed;\n    p.x = event.clientX;\n    p.y = event.clientY;\n    p.t = event.timeStamp;\n\n    window.clearTimeout(p.timer);\n    // Over the root's own background the box stays where it was.\n    const next = partAt(event.target, root, latest.current.selector);\n    if (!next || (next === s.point?.el && s.point.via === \"mouse\")) return;\n    const go = () => {\n      s.point = { el: next, via: \"mouse\" };\n      refresh();\n    };\n    if (p.speed < REDLINE.slow) go();\n    else p.timer = window.setTimeout(go, REDLINE.dwell);\n  };\n\n  const handlePointerLeave = (event: React.PointerEvent<HTMLDivElement>) => {\n    onPointerLeave?.(event);\n    if (event.pointerType !== \"mouse\") return;\n    const s = live.current;\n    window.clearTimeout(s.pointer.timer);\n    s.pointer.x = NaN;\n    s.pointer.speed = Infinity;\n    s.inside = false;\n    if (s.point?.via === \"mouse\" || s.point?.via === \"tour\") s.point = null;\n    refresh();\n    release();\n  };\n\n  /** Touch: tap to select, tap another to measure, tap the selected one again to clear. */\n  const tap = (el: Element | null) => {\n    const s = live.current;\n    engage();\n    if (!el || el === s.pinned) {\n      if (s.pinned || s.point) announce(\"Cleared\");\n      clear();\n      return;\n    }\n    if (!s.pinned) s.pinned = el;\n    s.point = { el, via: \"touch\" };\n    announce(describe(el));\n    refresh();\n  };\n\n  const handleClickCapture = (event: React.MouseEvent<HTMLDivElement>) => {\n    onClickCapture?.(event);\n    const root = rootRef.current;\n    if (!root) return;\n    // Clicks pin instead of pressing whatever is underneath.\n    event.preventDefault();\n    event.stopPropagation();\n    const s = live.current;\n    const el = partAt(event.target, root, latest.current.selector);\n    const keyboard = event.detail === 0;\n    if (!keyboard && s.pointer.kind !== \"mouse\") {\n      tap(el);\n      return;\n    }\n    window.clearTimeout(s.pointer.timer);\n    if (!el || el === s.pinned) {\n      if (s.pinned) announce(\"Unpinned\");\n      s.pinned = null;\n      if (el) s.point = { el, via: keyboard ? \"key\" : \"mouse\" };\n      refresh();\n      return;\n    }\n    s.pinned = el;\n    s.point = { el, via: keyboard ? \"key\" : \"mouse\" };\n    announce(`Pinned ${describe(el)}`);\n    refresh();\n  };\n\n  /* Keys: walk the tree, pin, clear. Only while the area itself has focus. */\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n    const root = rootRef.current;\n    if (!root || event.defaultPrevented || event.target !== root) return;\n    if (event.metaKey || event.ctrlKey) return;\n    const s = live.current;\n    const { selector } = latest.current;\n    const at = s.point?.el ?? s.pinned ?? null;\n\n    if (event.key === \"Escape\") {\n      if (!s.pinned && !s.point) return;\n      event.preventDefault();\n      if (s.pinned) s.pinned = null;\n      else s.point = null;\n      announce(s.point ? \"Unpinned\" : \"Cleared\");\n      refresh();\n      release();\n      return;\n    }\n\n    if (event.key === \"Enter\") {\n      if (!at) return;\n      event.preventDefault();\n      s.pinned = s.pinned === at ? null : at;\n      s.point = { el: at, via: \"key\" };\n      announce(s.pinned ? `Pinned ${describe(at)}` : \"Unpinned\");\n      refresh();\n      return;\n    }\n\n    const arrows = [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"];\n    if (!arrows.includes(event.key)) return;\n    event.preventDefault();\n    s.focused = true;\n    engage();\n    let to: Element | null = null;\n    if (!at) to = childParts(root, root, selector)[0] ?? null;\n    else if (event.key === \"ArrowUp\") to = parentPart(at, root, selector);\n    else if (event.key === \"ArrowDown\") to = childParts(at, root, selector)[0] ?? null;\n    else {\n      const siblings = childParts(parentPart(at, root, selector) ?? root, root, selector);\n      to = siblings[siblings.indexOf(at) + (event.key === \"ArrowRight\" ? 1 : -1)] ?? null;\n    }\n    if (!to) return;\n    s.point = { el: to, via: \"key\" };\n    announce(describe(to));\n    refresh();\n  };\n\n  return (\n    <div\n      {...props}\n      ref={rootRef}\n      tabIndex={0}\n      role=\"group\"\n      aria-label={ariaLabel}\n      aria-describedby={hintId}\n      className={cn(\n        \"relative isolate outline-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--redline-select)\",\n        className,\n      )}\n      style={\n        {\n          \"--redline-select\": colors?.selection ?? COLORS.selection,\n          \"--redline-distance\": colors?.distance ?? COLORS.distance,\n          \"--redline-space\": colors?.spacing ?? COLORS.spacing,\n          ...style,\n        } as React.CSSProperties\n      }\n      onPointerEnter={(event) => {\n        onPointerEnter?.(event);\n        if (event.pointerType !== \"mouse\") return;\n        live.current.inside = true;\n        engage();\n      }}\n      onPointerMove={handlePointerMove}\n      onPointerLeave={handlePointerLeave}\n      onPointerDown={(event) => {\n        onPointerDown?.(event);\n        live.current.pointer.kind = event.pointerType;\n      }}\n      onMouseDownCapture={(event) => {\n        onMouseDownCapture?.(event);\n        if (event.button !== 0) return;\n        // No focusing, dragging or text selection underneath; the area takes focus for Esc.\n        event.preventDefault();\n        rootRef.current?.focus({ preventScroll: true });\n      }}\n      onClickCapture={handleClickCapture}\n      onKeyDown={handleKeyDown}\n      onFocus={(event) => {\n        onFocus?.(event);\n        const root = rootRef.current;\n        if (!root || event.target !== root || !root.matches(\":focus-visible\")) return;\n        const s = live.current;\n        s.focused = true;\n        engage();\n        // Keyboard focus starts where the box already is, or on the first element.\n        const at = s.point?.el ?? s.pinned ?? childParts(root, root, latest.current.selector)[0];\n        if (!at) return;\n        s.point = { el: at, via: \"key\" };\n        announce(describe(at));\n        refresh();\n      }}\n      onBlur={(event) => {\n        onBlur?.(event);\n        if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n        const s = live.current;\n        if (!s.focused) return;\n        s.focused = false;\n        if (s.point?.via === \"key\") s.point = null;\n        refresh();\n        release();\n      }}\n    >\n      {children}\n\n      <OverlayContext.Provider value={{ instant: scene?.instant ?? true, reduced, tokens }}>\n        <div\n          data-redline-ui=\"\"\n          aria-hidden\n          className=\"pointer-events-none absolute top-0 left-0 z-50 size-0 overflow-visible font-sans select-none\"\n        >\n          <AnimatePresence>\n            {scene?.strips.map((strip) => <Hatch key={strip.key} strip={strip} />)}\n            {scene?.other && <Outline key=\"other\" rect={scene.other} />}\n            {scene?.guides.map((guide) => <GuideLine key={guide.key} guide={guide} />)}\n            {scene?.lines.map((line) => <Distance key={line.key} line={line} />)}\n            {scene && (\n              <SelectionBox key=\"main\" rect={scene.main} pill={scene.pill} card={scene.card} text={scene.text} />\n            )}\n          </AnimatePresence>\n        </div>\n      </OverlayContext.Provider>\n\n      <div data-redline-ui=\"\" className=\"sr-only\">\n        <span id={hintId}>\n          Arrow keys move between elements: up to the parent, down to the first child, left and right\n          to siblings. Enter pins one, and moving on then measures the distance from it. Escape clears.\n        </span>\n        <span role=\"status\" aria-live=\"polite\">\n          {message}\n        </span>\n      </div>\n    </div>\n  );\n}\n\n/* ── Overlay pieces ─────────────────────────────────────── */\n\ntype Overlay = { instant: boolean; reduced: boolean; tokens?: Tokens };\n\nconst OverlayContext = React.createContext<Overlay>({ instant: true, reduced: false });\n\n/** Lines stay crisp: positions land on whole device pixels. */\nconst snap = (value: number) => {\n  const dpr = window.devicePixelRatio || 1;\n  return Math.round(value * dpr) / dpr;\n};\n\n/** A number that glides to each new value, or jumps there when scrolling, resizing or reduced. */\nfunction useGlide(value: number) {\n  const { instant, reduced } = React.useContext(OverlayContext);\n  const mv = useMotionValue(value);\n  const jump = instant || reduced;\n  React.useEffect(() => {\n    if (jump) {\n      mv.jump(value);\n      return;\n    }\n    const controls = animate(mv, value, REDLINE.glide);\n    return () => controls.stop();\n  }, [mv, value, jump]);\n  return mv;\n}\n\nfunction useRect(rect: Rect) {\n  const x = useGlide(rect.x);\n  const y = useGlide(rect.y);\n  const w = useGlide(rect.w);\n  const h = useGlide(rect.h);\n  const style = {\n    x: useTransform(() => snap(x.get())),\n    y: useTransform(() => snap(y.get())),\n    width: useTransform(() => snap(x.get() + w.get()) - snap(x.get())),\n    height: useTransform(() => snap(y.get() + h.get()) - snap(y.get())),\n  };\n  return { w, h, style };\n}\n\n/** A label that ticks through whole numbers on its way to the new value. Token names swap instead. */\nfunction useTicker(value: MotionValue<number>, target: number, tokens?: Tokens) {\n  const final = named(target, tokens);\n  const isToken = final !== px(target);\n  const text = useMotionValue(final);\n  React.useEffect(() => {\n    const show = (v: number) =>\n      text.set(isToken || Math.abs(v - target) < 0.05 ? final : String(Math.round(v)));\n    show(value.get());\n    return value.on(\"change\", show);\n  }, [value, target, final, isToken, text]);\n  return text;\n}\n\nconst fade = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: { opacity: 0 },\n  transition: REDLINE.fade,\n};\n\nconst HANDLES = [\"-top-1 -left-1\", \"-top-1 -right-1\", \"-bottom-1 -left-1\", \"-bottom-1 -right-1\"];\n\nfunction SelectionBox({\n  rect,\n  pill,\n  card,\n  text,\n}: {\n  rect: Rect;\n  pill: Scene[\"pill\"];\n  card: Scene[\"card\"];\n  text: RedlineText | null;\n}) {\n  const { w, h, style } = useRect(rect);\n  const width = useTicker(w, rect.w);\n  const height = useTicker(h, rect.h);\n  return (\n    <motion.div className=\"absolute top-0 left-0\" style={style} {...fade}>\n      <div className=\"absolute -inset-px border border-(--redline-select)\" />\n      {HANDLES.map((place) => (\n        <div\n          key={place}\n          className={cn(\"absolute size-[7px] border border-(--redline-select) bg-white\", place)}\n        />\n      ))}\n      <div\n        className={cn(\n          \"absolute inset-x-0 flex justify-center\",\n          pill === \"below\" ? \"top-full pt-2\" : pill === \"above\" ? \"bottom-full pb-2\" : \"bottom-2\",\n        )}\n      >\n        <span className={cn(PILL, \"bg-(--redline-select)\")}>\n          <motion.span>{width}</motion.span>\n          {\" × \"}\n          <motion.span>{height}</motion.span>\n        </span>\n      </div>\n      <AnimatePresence>\n        {text && card && <SpecCard key=\"spec\" text={text} place={card} below={pill === \"below\"} />}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\nfunction SpecCard({\n  text,\n  place,\n  below,\n}: {\n  text: RedlineText;\n  place: NonNullable<Scene[\"card\"]>;\n  below: boolean;\n}) {\n  const [font, size, weight, tracking] = specOf(text);\n  const dot = <span className=\"text-zinc-500\">·</span>;\n  const { side, offset } = place;\n  return (\n    <motion.div\n      {...fade}\n      className={cn(\n        \"absolute flex\",\n        side === \"below\" ? \"inset-x-0 justify-center\" : \"-top-px\",\n        side === \"below\" && (below ? \"top-[calc(100%+34px)]\" : \"top-[calc(100%+10px)]\"),\n      )}\n      style={\n        side === \"right\"\n          ? { left: `calc(100% + ${offset}px)` }\n          : side === \"left\"\n            ? { right: `calc(100% + ${offset}px)` }\n            : undefined\n      }\n    >\n      <div className=\"flex items-center gap-1.5 rounded-[4px] bg-zinc-900 px-2 py-1 text-[11px]/4 font-medium whitespace-nowrap text-zinc-50 tabular-nums shadow-md ring-1 ring-white/10\">\n      <span className=\"max-w-32 truncate\">{font}</span>\n      {dot}\n      <span>{size}</span>\n      {dot}\n      <span>{weight}</span>\n      {dot}\n      <span>{tracking}</span>\n      {dot}\n      <span className=\"flex items-center gap-1\">\n        <span className=\"size-2.5 rounded-[2px] ring-1 ring-white/30\" style={{ background: text.color }} />\n        {text.color}\n      </span>\n      </div>\n    </motion.div>\n  );\n}\n\nfunction Outline({ rect }: { rect: Rect }) {\n  const { style } = useRect(rect);\n  return (\n    <motion.div className=\"absolute top-0 left-0\" style={style} {...fade}>\n      <div className=\"absolute -inset-px border border-(--redline-select) opacity-70\" />\n    </motion.div>\n  );\n}\n\nfunction Hatch({ strip }: { strip: Strip }) {\n  const { tokens } = React.useContext(OverlayContext);\n  const { w, h, style } = useRect(strip.rect);\n  const label = useTicker(strip.axis === \"x\" ? w : h, strip.value, tokens);\n  return (\n    <motion.div className=\"absolute top-0 left-0\" style={{ ...style, backgroundImage: HATCH }} {...fade}>\n      {strip.label && (\n        <div\n          className={cn(\n            \"absolute flex justify-center\",\n            strip.above ? \"inset-x-0 bottom-full pb-1\" : \"inset-0 items-center\",\n          )}\n        >\n          <motion.span className={cn(PILL, \"bg-(--redline-space)\")}>{label}</motion.span>\n        </div>\n      )}\n    </motion.div>\n  );\n}\n\nconst BESIDE = \"inset-y-0 left-full items-center pl-1.5\";\nconst BEFORE = \"inset-y-0 right-full items-center pr-1.5\";\n\n/** Where a line's label goes: beside it, on its other side, or past its outer end. */\nfunction labelPlace({ side, tag }: Line) {\n  if (side === \"top\" || side === \"bottom\") return tag === \"flip\" ? BEFORE : BESIDE;\n  if (tag === \"end\") return side === \"left\" ? BEFORE : BESIDE;\n  return tag === \"flip\"\n    ? \"inset-x-0 bottom-full justify-center pb-1.5\"\n    : \"inset-x-0 top-full justify-center pt-1.5\";\n}\n\nfunction Distance({ line }: { line: Line }) {\n  const { reduced, tokens } = React.useContext(OverlayContext);\n  const vertical = line.side === \"top\" || line.side === \"bottom\";\n  const x = useGlide(line.x);\n  const y = useGlide(line.y);\n  const length = useGlide(line.length);\n  const label = useTicker(length, line.length, tokens);\n  const sx = useTransform(() => snap(x.get()));\n  const sy = useTransform(() => snap(y.get()));\n  const span = useTransform(() =>\n    vertical ? snap(y.get() + length.get()) - snap(y.get()) : snap(x.get() + length.get()) - snap(x.get()),\n  );\n  return (\n    <motion.div\n      className=\"absolute top-0 left-0\"\n      style={vertical ? { x: sx, y: sy, width: 1, height: span } : { x: sx, y: sy, width: span, height: 1 }}\n      exit={{ opacity: 0 }}\n      transition={REDLINE.fade}\n    >\n      <motion.div\n        className={cn(\"absolute\", vertical ? \"inset-y-0 -left-[3px] w-[7px]\" : \"inset-x-0 -top-[3px] h-[7px]\")}\n        initial={reduced ? false : { clipPath: DRAW_FROM[line.side] }}\n        animate={{ clipPath: DRAWN }}\n        transition={REDLINE.draw}\n      >\n        <div\n          className={cn(\n            \"absolute bg-(--redline-distance)\",\n            vertical ? \"inset-y-0 left-[3px] w-px\" : \"inset-x-0 top-[3px] h-px\",\n          )}\n        />\n        {/* End ticks */}\n        <div className={cn(\"absolute bg-(--redline-distance)\", vertical ? \"inset-x-0 top-0 h-px\" : \"inset-y-0 left-0 w-px\")} />\n        <div className={cn(\"absolute bg-(--redline-distance)\", vertical ? \"inset-x-0 bottom-0 h-px\" : \"inset-y-0 right-0 w-px\")} />\n      </motion.div>\n      {/* Beside the line, not on it */}\n      <motion.div\n        className={cn(\"absolute flex\", labelPlace(line))}\n        initial={reduced ? false : { opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={{ ...REDLINE.fade, delay: reduced ? 0 : 0.14 }}\n      >\n        <motion.span className={cn(PILL, \"bg-(--redline-distance)\")}>{label}</motion.span>\n      </motion.div>\n    </motion.div>\n  );\n}\n\nfunction GuideLine({ guide }: { guide: Guide }) {\n  const vertical = guide.axis === \"y\";\n  const x = useGlide(guide.x);\n  const y = useGlide(guide.y);\n  const length = useGlide(guide.length);\n  const sx = useTransform(() => snap(x.get()));\n  const sy = useTransform(() => snap(y.get()));\n  const span = useTransform(() =>\n    vertical ? snap(y.get() + length.get()) - snap(y.get()) : snap(x.get() + length.get()) - snap(x.get()),\n  );\n  return (\n    <motion.div\n      className=\"absolute top-0 left-0\"\n      style={{\n        x: sx,\n        y: sy,\n        width: vertical ? 1 : span,\n        height: vertical ? span : 1,\n        backgroundImage: `repeating-linear-gradient(${vertical ? \"to bottom\" : \"to right\"}, var(--redline-distance) 0 3px, transparent 3px 6px)`,\n      }}\n      {...fade}\n    />\n  );\n}\n"
    }
  ]
}