{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-apply",
  "type": "registry:ui",
  "title": "Diff Apply",
  "description": "A code change that applies itself in place. Removed lines strike through and close up, new lines open in tinted, and the line numbers below roll to their new values. Each change then waits for Keep or Undo, and Undo plays it backwards.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "code",
    "developer",
    "ai"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/diff-apply",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/diff-apply.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, animate, motion, useReducedMotion, type AnimationPlaybackControls } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - The box reserves the taller version's height (the summed `data-ver` rows) and follows each\n *   change by scrolling only itself, so nothing on the page moves while the change plays.\n * - Removed lines close and new lines open on one height spring, so the code below slides as one\n *   block; Undo plays it back on the quicker `undo` spring, scaled to `duration`.\n * - Each timeline step only moves a hunk on from the status it expects, so a Keep or Undo\n *   mid-run wins over the timers still pending.\n * - The strike is a transparent copy of the line showing only its line-through, revealed left to\n *   right by `clipPath`, so wrapped lines strike too.\n * - Reduced motion (read only after hydration) drops the band, strike, fold, roll and scroll: the\n *   finished diff appears at once, tinted.\n */\nconst TIMING = {\n  /** Between one hunk starting and the next, in seconds (the `stagger` prop). */\n  stagger: 0.46,\n  /** The stagger's floor when a file has many hunks, in seconds. */\n  minStagger: 0.26,\n  /** The whole run aims to fit in this before the stagger tightens, in seconds. */\n  spread: 2.8,\n  /** After the run starts, before the first band lands, in seconds. */\n  start: 0.08,\n  /** The band sitting on a hunk before its lines move, in seconds. */\n  lead: 0.12,\n  /** A hunk staying tinted when `review` is off, in seconds. */\n  settle: 1.4,\n  /** The strike drawing across removed lines, and pulling back. */\n  strike: { duration: 0.22, ease: [0.65, 0, 0.35, 1] },\n  unstrike: { duration: 0.18, ease: \"easeOut\" },\n  /** Lines closing and opening; its `visualDuration` is the `duration` prop, in seconds. */\n  fold: { type: \"spring\", visualDuration: 0.32, bounce: 0 },\n  /** Undo, quicker than the edit. */\n  undo: { type: \"spring\", visualDuration: 0.28, bounce: 0 },\n  /** A changed line's own fade as it opens and closes. */\n  rowIn: { duration: 0.24, ease: \"easeOut\", delay: 0.04 },\n  rowOut: { duration: 0.16, ease: \"easeOut\" },\n  /** The red and green tints, the seam and the header counter fading. */\n  tint: { duration: 0.2, ease: \"easeOut\" },\n  /** Struck text dimming, as a Tailwind class. */\n  dim: \"duration-200\",\n  /** The band moving to the next hunk, fading in on the first and out after the last. */\n  band: { type: \"spring\", visualDuration: 0.3, bounce: 0 },\n  bandIn: { duration: 0.15, ease: \"easeOut\" },\n  bandOut: { duration: 0.18, ease: \"easeOut\" },\n  /** A line number rolling. */\n  roll: { type: \"spring\", visualDuration: 0.3, bounce: 0 },\n  /** Between line numbers in the wave, and the wave's cap, in seconds. */\n  rollStep: 0.008,\n  rollSpread: 0.15,\n  /** A hunk's Undo / Keep pill and the header's Undo all / Keep all showing up. */\n  pill: { type: \"spring\", visualDuration: 0.24, bounce: 0 },\n  /** The pill leaving, and fading in and out under reduced motion. */\n  pillOut: { duration: 0.12, ease: \"easeOut\" },\n  pillReduced: { duration: 0.12 },\n  /** The header's Undo all / Keep all leaving. */\n  actionsOut: { duration: 0.12 },\n  /** The review strip's fade as it opens and closes, in seconds, and its curve. */\n  stripFade: { open: 0.2, close: 0.12, ease: \"easeOut\" },\n  /** The box scrolling a hunk into view. */\n  scroll: { duration: 0.45, ease: [0.22, 1, 0.36, 1] },\n  /** The file fading back to `before` on replay. */\n  replay: { duration: 0.15, ease: \"easeOut\" },\n  /** Buttons' hover color and opacity, as a Tailwind class. */\n  press: \"duration-150\",\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * DIFF APPLY STORYBOARD\n *\n *  rest     an editor card: the file's tab, line numbers and the\n *           `before` code, highlighted. Complete on its own. The\n *           box already holds the taller of the two versions, so\n *           nothing below it moves while the change plays\n *  view     40% in view, the change plays one hunk at a time,\n *           0.46s apart (tighter when there are many):\n *    0ms    a soft band glides from the last hunk onto this one\n *           and the box scrolls it into view if it has to\n *  120ms    removed lines tint red and a strike draws across them,\n *           left to right, in 220ms\n *  340ms    they close while the new lines open under them, tinted\n *           green, on one spring (0.32s, no bounce), so the code\n *           below slides as one block. The \"+N −M\" counter ticks\n *           and the line numbers below roll to their new values,\n *           a wave 8ms per line, top to bottom\n *  review   each hunk that lands gets a small Undo / Keep pill at\n *           its right edge, and the header offers Undo all / Keep\n *           all. Keep fades the tint in 200ms. Undo plays the hunk\n *           backwards on a quicker spring (0.28s): the new lines\n *           close, the old ones reopen and the strike pulls back.\n *           Both work while later hunks are still playing\n *  settle   with `review` off, tints fade 1.4s after a hunk lands\n *  again    Replay fades the file back to `before` and plays it\n *           again; `play` drives the same thing from outside\n *  keys     every control is a button in reading order: header\n *           actions, then each hunk's Undo and Keep. Screen readers\n *           get the file's current text and hear \"3 changes\n *           applied\", \"Change 2 undone\"\n *  reduced  no band, strike, fold or roll: the finished diff appears\n *           at once, tinted, and Keep / Undo swap instantly\n * ───────────────────────────────────────────────────────── */\n\nconst DIFF = {\n  view: 0.4, // share of the block in view before it plays\n  inlineBelow: 544, // px; narrower boxes get review strips instead of floating pills\n} as const satisfies Record<string, number>;\n\n// The fold at a given `duration`, and undo kept quicker in the same proportion.\nconst foldOf = (duration: number) => ({ ...TIMING.fold, visualDuration: duration });\nconst undoOf = (duration: number) => ({\n  ...TIMING.undo,\n  visualDuration: TIMING.undo.visualDuration * (duration / TIMING.fold.visualDuration),\n});\n\n/* ── Public types ───────────────────────────────────────── */\n\n/** What the visitor decided for one change. */\nexport type DiffApplyDecision = \"pending\" | \"kept\" | \"undone\";\n\nexport interface DiffApplyProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** The file as it was. */\n  before: string;\n  /** The file after the edit. The component works out the changed lines itself. */\n  after: string;\n  /** Shown in the tab. Its extension picks the highlighting when `language` isn't set. */\n  filename?: string;\n  /** Highlighting: `ts`/`js`, `json`, `css` or `md`. Anything else is plain. Defaults to the filename's extension. */\n  language?: string;\n  /** After a change lands, wait for Keep or Undo. Off, the tint fades on its own and the file reads as finished. */\n  review?: boolean;\n  /** Leave unset to play once the block is 40% in view. Set it to drive the block yourself: `true` plays the change, `false` puts the file back to `before`. */\n  play?: boolean;\n  /** Show a Replay button once the change has played. */\n  replay?: boolean;\n  /** Wrap long lines instead of scrolling sideways. Use it for prose. */\n  wrap?: boolean;\n  /** Show the line-number gutter. */\n  lineNumbers?: boolean;\n  /** Cap the code box's height (px or any CSS length). The box scrolls inside and follows each change into view. */\n  maxHeight?: number | string;\n  /** Seconds between one change starting and the next. Files with many changes tighten it, so the whole run stays short. */\n  stagger?: number;\n  /** Seconds removed lines take to close and new lines to open. Undo plays back a little quicker. */\n  duration?: number;\n  /** Called once every change has landed. */\n  onComplete?: () => void;\n  /** Called after each Keep or Undo with the decision for every change, in order. */\n  onReview?: (decisions: DiffApplyDecision[]) => void;\n}\n\n/* ── Diff ───────────────────────────────────────────────── */\n\ntype Op = { kind: \"same\" | \"del\" | \"add\"; text: string; at: number }; // at: line index in its own version\n\n/** A line diff by longest common subsequence: plenty for files of a few hundred lines. */\nfunction diffLines(a: string[], b: string[]): Op[] {\n  const n = a.length;\n  const m = b.length;\n  const table: Uint32Array[] = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));\n  for (let i = n - 1; i >= 0; i--) {\n    for (let j = m - 1; j >= 0; j--) {\n      table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);\n    }\n  }\n  const ops: Op[] = [];\n  let i = 0;\n  let j = 0;\n  while (i < n && j < m) {\n    if (a[i] === b[j]) {\n      ops.push({ kind: \"same\", text: b[j], at: j });\n      i++;\n      j++;\n    } else if (table[i + 1][j] >= table[i][j + 1]) {\n      ops.push({ kind: \"del\", text: a[i], at: i++ });\n    } else {\n      ops.push({ kind: \"add\", text: b[j], at: j++ });\n    }\n  }\n  while (i < n) ops.push({ kind: \"del\", text: a[i], at: i++ });\n  while (j < m) ops.push({ kind: \"add\", text: b[j], at: j++ });\n  return ops;\n}\n\n/* ── Highlighting ───────────────────────────────────────── */\n\ntype TokenType = \"plain\" | \"keyword\" | \"string\" | \"number\" | \"comment\" | \"function\" | \"property\" | \"type\" | \"punctuation\";\ntype Token = { type: TokenType; text: string };\n\n// One map to retheme the highlighter, the same hues as Polyglot.\nconst TOKEN_CLASS: Record<TokenType, string> = {\n  plain: \"\",\n  keyword: \"text-rose-600 dark:text-rose-400\",\n  string: \"text-emerald-700 dark:text-emerald-300\",\n  number: \"text-blue-600 dark:text-sky-300\",\n  comment: \"text-muted-foreground italic\",\n  function: \"text-violet-600 dark:text-violet-300\",\n  property: \"text-amber-700 dark:text-amber-200\",\n  type: \"text-cyan-700 dark:text-cyan-300\",\n  punctuation: \"text-muted-foreground\",\n};\n\ntype Lang = \"js\" | \"json\" | \"css\" | \"md\" | \"text\";\n\nconst LANGS: Record<string, Lang> = {\n  js: \"js\",\n  jsx: \"js\",\n  ts: \"js\",\n  tsx: \"js\",\n  mjs: \"js\",\n  cjs: \"js\",\n  mts: \"js\",\n  javascript: \"js\",\n  typescript: \"js\",\n  json: \"json\",\n  jsonc: \"json\",\n  css: \"css\",\n  md: \"md\",\n  mdx: \"md\",\n  markdown: \"md\",\n};\n\nconst toLang = (language: string | undefined, filename: string | undefined): Lang => {\n  const key = (language ?? filename?.split(\".\").pop() ?? \"\").toLowerCase();\n  return LANGS[key] ?? \"text\";\n};\n\nconst words = (list: string) => new Set(list.split(\" \"));\nconst JS_KEYWORDS = words(\n  \"as async await break case catch class const continue default delete do else enum export extends finally for from function if import in instanceof interface let new of return satisfies static switch throw try type typeof var void while yield\",\n);\nconst JS_CONSTANTS = words(\"true false null undefined this\");\nconst JS_TYPES = words(\"string number boolean any unknown never void Promise Record\");\n\ntype Push = (type: TokenType, text: string) => void;\n\nconst isSpace = (c: string | undefined) => c === \" \" || c === \"\\t\" || c === \"\\n\" || c === \"\\r\";\n\nfunction nextNonSpace(code: string, from: number) {\n  let i = from;\n  while (i < code.length && (code[i] === \" \" || code[i] === \"\\t\")) i++;\n  return i;\n}\n\nfunction readString(code: string, i: number, quote: string) {\n  let j = i + 1;\n  while (j < code.length) {\n    if (code[j] === \"\\\\\") {\n      j += 2;\n      continue;\n    }\n    if (code[j] === quote) return j + 1;\n    if (code[j] === \"\\n\" && quote !== \"`\") return j;\n    j++;\n  }\n  return code.length;\n}\n\nfunction lexJs(code: string, push: Push) {\n  let i = 0;\n  let last = \"\";\n  const emit = (type: TokenType, text: string) => {\n    push(type, text);\n    const trimmed = text.trimEnd();\n    if (trimmed) last = trimmed[trimmed.length - 1];\n  };\n  while (i < code.length) {\n    const c = code[i];\n    if (isSpace(c)) {\n      let j = i;\n      while (isSpace(code[j])) j++;\n      push(\"plain\", code.slice(i, j));\n      i = j;\n    } else if (code.startsWith(\"//\", i)) {\n      const end = code.indexOf(\"\\n\", i);\n      const j = end < 0 ? code.length : end;\n      emit(\"comment\", code.slice(i, j));\n      i = j;\n    } else if (code.startsWith(\"/*\", i)) {\n      const end = code.indexOf(\"*/\", i + 2);\n      const j = end < 0 ? code.length : end + 2;\n      emit(\"comment\", code.slice(i, j));\n      i = j;\n    } else if (c === '\"' || c === \"'\" || c === \"`\") {\n      const j = readString(code, i, c);\n      const after = nextNonSpace(code, j);\n      emit(code[after] === \":\" && c !== \"`\" ? \"property\" : \"string\", code.slice(i, j));\n      i = j;\n    } else if (/\\d/.test(c)) {\n      const m = /(?:0[xob][\\da-f_]+|\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?)n?/iy;\n      m.lastIndex = i;\n      const text = m.exec(code)?.[0] || c;\n      emit(\"number\", text);\n      i += text.length;\n    } else if (/[A-Za-z_$]/.test(c)) {\n      let j = i;\n      while (j < code.length && /[\\w$]/.test(code[j])) j++;\n      const word = code.slice(i, j);\n      const after = nextNonSpace(code, j);\n      let type: TokenType = \"plain\";\n      if (JS_KEYWORDS.has(word) && last !== \".\") type = \"keyword\";\n      else if (JS_CONSTANTS.has(word)) type = \"number\";\n      else if (code[j] === \"(\") type = \"function\";\n      else if (code[after] === \":\" && code[after + 1] !== \":\") type = \"property\";\n      else if (last === \".\") type = \"property\";\n      else if (/^[A-Z]/.test(word) || JS_TYPES.has(word)) type = \"type\";\n      emit(type, word);\n      i = j;\n    } else {\n      emit(\"punctuation\", c);\n      i++;\n    }\n  }\n}\n\nfunction lexJson(code: string, push: Push) {\n  let i = 0;\n  while (i < code.length) {\n    const c = code[i];\n    if (isSpace(c)) {\n      let j = i;\n      while (isSpace(code[j])) j++;\n      push(\"plain\", code.slice(i, j));\n      i = j;\n    } else if (c === '\"') {\n      const j = readString(code, i, '\"');\n      push(code[nextNonSpace(code, j)] === \":\" ? \"property\" : \"string\", code.slice(i, j));\n      i = j;\n    } else if (/[-\\d]/.test(c)) {\n      const m = /-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/y;\n      m.lastIndex = i;\n      const text = m.exec(code)?.[0] || c;\n      push(text === c && !/\\d/.test(c) ? \"punctuation\" : \"number\", text);\n      i += text.length;\n    } else if (/[a-z]/i.test(c)) {\n      let j = i;\n      while (j < code.length && /[a-z]/i.test(code[j])) j++;\n      push(\"number\", code.slice(i, j));\n      i = j;\n    } else {\n      push(\"punctuation\", c);\n      i++;\n    }\n  }\n}\n\nfunction lexCss(code: string, push: Push) {\n  let i = 0;\n  let depth = 0;\n  let value = false; // after a property's colon, until ; or }\n  while (i < code.length) {\n    const c = code[i];\n    if (isSpace(c)) {\n      let j = i;\n      while (isSpace(code[j])) j++;\n      push(\"plain\", code.slice(i, j));\n      i = j;\n    } else if (code.startsWith(\"/*\", i)) {\n      const end = code.indexOf(\"*/\", i + 2);\n      const j = end < 0 ? code.length : end + 2;\n      push(\"comment\", code.slice(i, j));\n      i = j;\n    } else if (c === '\"' || c === \"'\") {\n      const j = readString(code, i, c);\n      push(\"string\", code.slice(i, j));\n      i = j;\n    } else if (c === \"{\" || c === \"}\" || c === \";\" || c === \":\" || c === \",\" || c === \"(\" || c === \")\") {\n      if (c === \"{\") depth++;\n      if (c === \"}\") depth = Math.max(0, depth - 1);\n      if (c === \":\" && depth > 0) value = true;\n      if (c === \";\" || c === \"}\" || c === \"{\") value = false;\n      push(\"punctuation\", c);\n      i++;\n    } else if (c === \"@\") {\n      let j = i + 1;\n      while (j < code.length && /[\\w-]/.test(code[j])) j++;\n      push(\"keyword\", code.slice(i, j));\n      i = j;\n    } else if (value && (c === \"#\" || /[\\d.-]/.test(c))) {\n      const m = /#[\\da-f]{3,8}|-?\\d*\\.?\\d+(?:[a-z%]+)?/iy;\n      m.lastIndex = i;\n      const text = m.exec(code)?.[0] || c;\n      push(\"number\", text);\n      i += text.length;\n    } else {\n      let j = i;\n      while (j < code.length && !isSpace(code[j]) && !\"{}:;,()\\\"'\".includes(code[j])) j++;\n      if (j === i) j++;\n      const word = code.slice(i, j);\n      if (value) push(code[j] === \"(\" ? \"function\" : \"plain\", word);\n      else if (depth > 0 && code[nextNonSpace(code, j)] === \":\") push(\"property\", word);\n      else push(\"function\", word); // a selector\n      i = j;\n    }\n  }\n}\n\nfunction lexMarkdownLine(line: string, push: Push) {\n  const heading = /^(#{1,6})(\\s.*)$/.exec(line);\n  if (heading) {\n    push(\"punctuation\", heading[1]);\n    push(\"function\", heading[2]);\n    return;\n  }\n  if (/^\\s*>/.test(line)) {\n    push(\"comment\", line);\n    return;\n  }\n  let rest = line;\n  const marker = /^(\\s*)([-*+]|\\d+\\.)(\\s)/.exec(rest);\n  if (marker) {\n    push(\"plain\", marker[1]);\n    push(\"punctuation\", marker[2]);\n    push(\"plain\", marker[3]);\n    rest = rest.slice(marker[0].length);\n  }\n  const inline = /(`[^`]+`)|(\\*\\*[^*]+\\*\\*)|(\\[[^\\]]+\\]\\([^)]+\\))/g;\n  let k = 0;\n  for (const m of rest.matchAll(inline)) {\n    push(\"plain\", rest.slice(k, m.index));\n    if (m[1]) push(\"string\", m[1]);\n    else if (m[2]) push(\"property\", m[2]);\n    else {\n      const close = m[0].indexOf(\"](\");\n      push(\"plain\", m[0].slice(0, close + 1));\n      push(\"type\", m[0].slice(close + 1));\n    }\n    k = (m.index ?? 0) + m[0].length;\n  }\n  push(\"plain\", rest.slice(k));\n}\n\n/** Tokenizes a whole version and cuts it into lines, so comments and template strings that span lines still color right. */\nfunction highlight(lines: string[], lang: Lang): Token[][] {\n  if (lang === \"text\") return lines.map((text) => [{ type: \"plain\", text }]);\n  if (lang === \"md\") {\n    return lines.map((text) => {\n      const out: Token[] = [];\n      lexMarkdownLine(text, (type, t) => {\n        if (!t) return;\n        const prev = out[out.length - 1];\n        if (prev && prev.type === type) prev.text += t;\n        else out.push({ type, text: t });\n      });\n      return out;\n    });\n  }\n  const code = lines.join(\"\\n\");\n  const types: TokenType[] = [];\n  const push: Push = (type, text) => {\n    for (let k = 0; k < text.length; k++) types.push(type);\n  };\n  if (lang === \"json\") lexJson(code, push);\n  else if (lang === \"css\") lexCss(code, push);\n  else lexJs(code, push);\n  const out: Token[][] = [];\n  let offset = 0;\n  for (const line of lines) {\n    const row: Token[] = [];\n    for (let k = 0; k < line.length; k++) {\n      const type = types[offset + k] ?? \"plain\";\n      const prev = row[row.length - 1];\n      if (prev && prev.type === type) prev.text += line[k];\n      else row.push({ type, text: line[k] });\n    }\n    out.push(row);\n    offset += line.length + 1;\n  }\n  return out;\n}\n\n/* ── Model ──────────────────────────────────────────────── */\n\ntype Row = { key: string; kind: Op[\"kind\"]; tokens: Token[]; hunk: number; index: number }; // index: its place in display order\ntype Hunk = { dels: Row[]; adds: Row[]; first: number }; // first: index of its first row\ntype Part = { kind: \"same\"; row: Row } | { kind: \"hunk\"; index: number };\ntype Model = { rows: Row[]; parts: Part[]; hunks: Hunk[]; before: string[]; after: string[] };\n\n// Drops the first line when it's empty and trailing whitespace, so template literals read naturally.\nconst clean = (text: string) => text.replace(/\\r\\n?/g, \"\\n\").replace(/^[ \\t]*\\n/, \"\").trimEnd();\n\nfunction buildModel(before: string, after: string, lang: Lang): Model {\n  const a = clean(before).split(\"\\n\");\n  const b = clean(after).split(\"\\n\");\n  const ta = highlight(a, lang);\n  const tb = highlight(b, lang);\n  const parts: Part[] = [];\n  const hunks: Hunk[] = [];\n  let open: Hunk | null = null;\n  for (const op of diffLines(a, b)) {\n    if (op.kind === \"same\") {\n      open = null;\n      parts.push({ kind: \"same\", row: { key: `s${op.at}`, kind: \"same\", tokens: tb[op.at], hunk: -1, index: 0 } });\n      continue;\n    }\n    if (!open) {\n      open = { dels: [], adds: [], first: 0 };\n      hunks.push(open);\n      parts.push({ kind: \"hunk\", index: hunks.length - 1 });\n    }\n    const del = op.kind === \"del\";\n    (del ? open.dels : open.adds).push({\n      key: `${del ? \"d\" : \"a\"}${op.at}`,\n      kind: op.kind,\n      tokens: del ? ta[op.at] : tb[op.at],\n      hunk: hunks.length - 1,\n      index: 0,\n    });\n  }\n  // Display order: inside a hunk, the removed lines sit above the new ones.\n  const rows: Row[] = [];\n  for (const part of parts) {\n    if (part.kind === \"same\") rows.push(part.row);\n    else {\n      const hunk = hunks[part.index];\n      hunk.first = rows.length;\n      rows.push(...hunk.dels, ...hunk.adds);\n    }\n  }\n  rows.forEach((row, i) => (row.index = i));\n  return { rows, parts, hunks, before: a, after: b };\n}\n\n/* ── Run state ──────────────────────────────────────────── */\n\ntype Status = \"pending\" | \"striking\" | \"applied\" | \"kept\" | \"undone\";\n\ntype Run = {\n  token: number; // bumps on every play; 0 means it never played\n  statuses: Status[];\n  active: number | null; // the hunk under the band\n  playing: boolean;\n  wave: number; // first row of the hunk that changed last, where the line-number wave starts\n  message: string;\n};\n\nconst landed = (s: Status) => s === \"applied\" || s === \"kept\";\nconst decisionOf = (s: Status): DiffApplyDecision => (s === \"kept\" ? \"kept\" : s === \"undone\" ? \"undone\" : \"pending\");\nconst plural = (n: number, one: string) => `${n} ${one}${n === 1 ? \"\" : \"s\"}`;\nconst noopSubscribe = () => () => {};\n\n/* ── DiffApply ──────────────────────────────────────────── */\n\nexport function DiffApply({\n  before,\n  after,\n  filename,\n  language,\n  review = true,\n  play,\n  replay = true,\n  wrap = false,\n  lineNumbers = true,\n  maxHeight,\n  stagger: staggerProp = TIMING.stagger,\n  duration = TIMING.fold.visualDuration,\n  onComplete,\n  onReview,\n  className,\n  ...props\n}: DiffApplyProps) {\n  const hydrated = React.useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const prefersReduced = useReducedMotion() ?? false;\n  const reduced = prefersReduced && hydrated;\n\n  const lang = toLang(language, filename);\n  const model = React.useMemo(() => buildModel(before, after, lang), [before, after, lang]);\n  const count = model.hunks.length;\n  const idle = (): Status[] => Array.from({ length: count }, () => \"pending\");\n\n  const [run, setRun] = React.useState<Run>(() => ({\n    token: 0,\n    statuses: idle(),\n    active: null,\n    playing: false,\n    wave: 0,\n    message: \"\",\n  }));\n  // New text starts over from `before`.\n  const [modelSeen, setModelSeen] = React.useState(model);\n  if (modelSeen !== model) {\n    setModelSeen(model);\n    setRun((r) => ({ ...r, statuses: idle(), active: null, playing: false }));\n  }\n  const statuses = run.statuses.length === count ? run.statuses : idle();\n\n  const start = React.useCallback(() => {\n    setRun((r) => ({\n      token: r.token + 1,\n      statuses: Array.from({ length: count }, () => \"pending\"),\n      active: null,\n      playing: count > 0,\n      wave: 0,\n      message: \"\",\n    }));\n  }, [count]);\n\n  const reset = React.useCallback(() => {\n    setRun((r) => ({\n      token: r.token + 1,\n      statuses: Array.from({ length: count }, () => \"pending\"),\n      active: null,\n      playing: false,\n      wave: 0,\n      message: \"\",\n    }));\n  }, [count]);\n\n  // `play` from outside: adjust during render, the effect below does the timing.\n  // Starts unset, so `play` already true on mount plays right away.\n  const [playSeen, setPlaySeen] = React.useState<boolean | undefined>(undefined);\n  if (play !== playSeen) {\n    setPlaySeen(play);\n    if (play === true) start();\n    else if (play === false) reset();\n  }\n\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const scrollerRef = React.useRef<HTMLDivElement>(null);\n  const contentRef = React.useRef<HTMLDivElement>(null);\n  const timers = React.useRef<number[]>([]);\n  const scrollAnim = React.useRef<AnimationPlaybackControls | null>(null);\n  const bandHost = React.useRef<HTMLElement | null>(null);\n  // The hunk the band sat on last, so the next band can fly in from it.\n  const swapBandHost = React.useCallback((el: HTMLElement) => {\n    const prev = bandHost.current;\n    bandHost.current = el;\n    return prev;\n  }, []);\n  const callbacks = React.useRef({ onComplete, onReview });\n  React.useEffect(() => {\n    callbacks.current = { onComplete, onReview };\n  });\n\n  const clearTimers = React.useCallback(() => {\n    timers.current.forEach((id) => window.clearTimeout(id));\n    timers.current = [];\n  }, []);\n\n  // Plays once 40% of the block is in view, unless `play` drives it.\n  const auto = play === undefined;\n  React.useEffect(() => {\n    const root = rootRef.current;\n    if (!auto || !root) return;\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (!entry?.isIntersecting) return;\n        observer.disconnect();\n        setRun((r) =>\n          r.token === 0\n            ? { token: 1, statuses: Array.from({ length: count }, () => \"pending\"), active: null, playing: count > 0, wave: 0, message: \"\" }\n            : r,\n        );\n      },\n      { threshold: DIFF.view },\n    );\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, [auto, count]);\n\n  // Keeps the hunk being edited in view inside the box, never by scrolling the page.\n  const reveal = React.useCallback(\n    (index: number) => {\n      const scroller = scrollerRef.current;\n      const content = contentRef.current;\n      const host = content?.querySelector<HTMLElement>(`[data-hunk=\"${index}\"]`);\n      if (!scroller || !host || scroller.scrollHeight <= scroller.clientHeight + 1) return;\n      const hunk = model.hunks[index];\n      const line = 24;\n      const span = Math.max(hunk.dels.length, hunk.adds.length, 1) * line;\n      const view = scroller.clientHeight;\n      const top = host.offsetTop;\n      if (top >= scroller.scrollTop + line && top + span <= scroller.scrollTop + view - line) return;\n      const target = Math.max(0, Math.min(top - (view - span) / 2, scroller.scrollHeight - view));\n      scrollAnim.current?.stop();\n      scrollAnim.current = reduced\n        ? null\n        : animate(scroller.scrollTop, target, {\n            ...TIMING.scroll,\n            onUpdate: (v) => {\n              scroller.scrollTop = v;\n            },\n          });\n      if (reduced) scroller.scrollTop = target;\n    },\n    [model, reduced],\n  );\n\n  // A visitor scrolling the box takes it over from the follow.\n  React.useEffect(() => {\n    const scroller = scrollerRef.current;\n    if (!scroller) return;\n    const stop = () => scrollAnim.current?.stop();\n    scroller.addEventListener(\"wheel\", stop, { passive: true });\n    scroller.addEventListener(\"touchstart\", stop, { passive: true });\n    return () => {\n      scroller.removeEventListener(\"wheel\", stop);\n      scroller.removeEventListener(\"touchstart\", stop);\n    };\n  }, []);\n\n  // The timeline for one run. Every step is a guarded state change, so Undo\n  // or Keep mid-run simply wins.\n  React.useEffect(() => {\n    if (run.token === 0 || !run.playing) return;\n    const at = (seconds: number, fn: () => void) => {\n      timers.current.push(window.setTimeout(fn, seconds * 1000));\n    };\n    const setStatus = (index: number, from: Status[], to: Status) =>\n      setRun((r) => {\n        if (!from.includes(r.statuses[index])) return r;\n        const next = r.statuses.slice();\n        next[index] = to;\n        return { ...r, statuses: next, wave: model.hunks[index].first };\n      });\n    const finish = () => {\n      setRun((r) => {\n        const n = r.statuses.filter(landed).length;\n        return { ...r, active: null, playing: false, message: n ? `${plural(n, \"change\")} applied` : r.message };\n      });\n      callbacks.current.onComplete?.();\n    };\n\n    if (reduced) {\n      at(0, () => {\n        setRun((r) => {\n          const next = r.statuses.map((s): Status => (s === \"pending\" || s === \"striking\" ? (review ? \"applied\" : \"kept\") : s));\n          return { ...r, statuses: next, wave: 0 };\n        });\n        finish();\n      });\n      return clearTimers;\n    }\n\n    // `stagger` scales the floor and the spread with it, so many changes still tighten the same way.\n    const pace = Math.max(0, staggerProp) / TIMING.stagger;\n    const stagger = Math.min(\n      TIMING.stagger * pace,\n      Math.max(TIMING.minStagger * pace, (TIMING.spread * pace) / Math.max(1, count)),\n    );\n    let end = 0;\n    model.hunks.forEach((hunk, i) => {\n      const t = TIMING.start + i * stagger;\n      at(t, () => {\n        setRun((r) => (r.statuses[i] === \"pending\" ? { ...r, active: i } : r));\n        reveal(i);\n      });\n      const strike = hunk.dels.length ? TIMING.strike.duration : 0;\n      if (hunk.dels.length) at(t + TIMING.lead, () => setStatus(i, [\"pending\"], \"striking\"));\n      const land = t + TIMING.lead + strike;\n      at(land, () => setStatus(i, [\"pending\", \"striking\"], \"applied\"));\n      if (!review) at(land + TIMING.settle, () => setStatus(i, [\"applied\"], \"kept\"));\n      end = land + duration;\n    });\n    at(end, finish);\n    return clearTimers;\n  }, [run.token, run.playing, model, count, review, reduced, reveal, clearTimers, staggerProp, duration]);\n\n  React.useEffect(() => () => scrollAnim.current?.stop(), []);\n\n  /* decisions */\n  const decide = (indices: number[], to: \"kept\" | \"undone\", message: string) => {\n    const pick = (s: Status, k: number) =>\n      indices.includes(k) && (to === \"kept\" ? s !== \"kept\" && s !== \"undone\" : s !== \"undone\" && s !== \"kept\") ? to : s;\n    const next = statuses.map(pick);\n    const stillPlaying = run.playing && next.some((s) => s === \"pending\" || s === \"striking\");\n    if (!stillPlaying) clearTimers();\n    setRun((r) => {\n      const statusesNext = r.statuses.map(pick);\n      return {\n        ...r,\n        statuses: statusesNext,\n        playing: stillPlaying,\n        active: stillPlaying ? r.active : null,\n        wave: indices.length ? model.hunks[indices[0]].first : r.wave,\n        message,\n      };\n    });\n    callbacks.current.onReview?.(next.map(decisionOf));\n    if (run.playing && !stillPlaying && to === \"kept\") callbacks.current.onComplete?.();\n    // The button that was pressed goes away: hand focus to the next change\n    // still waiting, or back to the code.\n    const root = rootRef.current;\n    if (root && root.contains(document.activeElement) && document.activeElement !== root) {\n      const after = indices[indices.length - 1] ?? -1;\n      const order = [...next.keys()].filter((k) => next[k] === \"applied\");\n      const target = order.find((k) => k > after) ?? order[0];\n      requestAnimationFrame(() => {\n        const button =\n          target === undefined ? null : root.querySelector<HTMLElement>(`[data-keep=\"${target}\"]`);\n        (button ?? scrollerRef.current)?.focus({ preventScroll: true });\n      });\n    }\n  };\n\n  const waiting = statuses.map((s, i) => (s === \"applied\" ? i : -1)).filter((i) => i >= 0);\n  const all = model.hunks.map((_, i) => i);\n  const adds = model.hunks.reduce((sum, h, i) => sum + (landed(statuses[i]) ? h.adds.length : 0), 0);\n  const dels = model.hunks.reduce((sum, h, i) => sum + (landed(statuses[i]) ? h.dels.length : 0), 0);\n  const touched = statuses.some((s) => s !== \"pending\");\n  const finished = run.token > 0 && !run.playing;\n\n  /* the box holds the taller version, so the card never resizes mid-run */\n  const [reserve, setReserve] = React.useState(0);\n  // Pills float over the code's right edge in a wide box. Prose fills the line\n  // and a narrow box has no spare edge, so there they open as a strip under the\n  // change instead of covering words.\n  const [narrow, setNarrow] = React.useState(false);\n  const inline = wrap || narrow;\n  // Before it can measure (the server render), every line is one 24px row.\n  const estimate = Math.max(model.before.length, model.after.length) * 24;\n  React.useLayoutEffect(() => {\n    const scroller = scrollerRef.current;\n    if (!scroller) return;\n    const measure = () => {\n      const content = contentRef.current;\n      if (!content) return;\n      let a = 0;\n      let b = 0;\n      content.querySelectorAll<HTMLElement>(\"[data-ver]\").forEach((el) => {\n        const h = el.offsetHeight;\n        const ver = el.dataset.ver;\n        if (ver !== \"after\") a += h;\n        if (ver !== \"before\") b += h;\n      });\n      setReserve(Math.ceil(Math.max(a, b)));\n      setNarrow(scroller.clientWidth < DIFF.inlineBelow);\n      scroller.style.setProperty(\"--diff-view\", `${scroller.clientWidth}px`);\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(scroller);\n    document.fonts?.ready.then(measure).catch(() => {});\n    return () => observer.disconnect();\n  }, [model, wrap, lineNumbers, inline, review]);\n\n  /* line numbers: count the lines that are in the file right now */\n  const digits = String(Math.max(model.before.length, model.after.length)).length;\n  const numbers: (number | null)[] = [];\n  {\n    let n = 0;\n    for (const row of model.rows) {\n      const s = row.hunk >= 0 ? statuses[row.hunk] : \"pending\";\n      const live = row.kind === \"same\" || (row.kind === \"del\" ? !landed(s) : landed(s));\n      numbers.push(live ? ++n : null);\n    }\n  }\n\n  // What a screen reader gets: the file as it reads right now.\n  const current = model.parts\n    .flatMap((part) => {\n      if (part.kind === \"same\") return [part.row];\n      const hunk = model.hunks[part.index];\n      return landed(statuses[part.index]) ? hunk.adds : hunk.dels;\n    })\n    .map((row) => row.tokens.map((t) => t.text).join(\"\"))\n    .join(\"\\n\");\n\n  const rowProps = { digits, lineNumbers, wrap, reduced, wave: run.wave };\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"@container/diff relative w-full overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04),0_12px_32px_-16px_rgb(0_0_0/0.14)]\",\n        className,\n      )}\n      {...props}\n    >\n      {/* header */}\n      <div className=\"flex h-11 items-center gap-3 border-b border-border bg-muted/40 pr-1.5 pl-3.5\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          <svg aria-hidden viewBox=\"0 0 16 16\" className=\"size-3.5 shrink-0 text-muted-foreground\" fill=\"none\">\n            <path\n              d=\"M4 1.75h5.25L12.5 5v8.25a1 1 0 0 1-1 1h-7.5a1 1 0 0 1-1-1V2.75a1 1 0 0 1 1-1Z\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.25\"\n              strokeLinejoin=\"round\"\n            />\n            <path d=\"M9 1.75V5.25h3.5\" stroke=\"currentColor\" strokeWidth=\"1.25\" strokeLinejoin=\"round\" />\n          </svg>\n          <span className=\"truncate text-[13px] font-medium\">{filename ?? \"Untitled\"}</span>\n        </div>\n        <motion.div\n          aria-hidden\n          initial={false}\n          animate={{ opacity: touched ? 1 : 0 }}\n          transition={TIMING.tint}\n          className=\"flex shrink-0 items-center gap-1.5 font-mono text-[12px] tabular-nums\"\n        >\n          <span className=\"flex text-emerald-600 dark:text-emerald-400\">\n            +<Roll value={adds} reduced={reduced} />\n          </span>\n          <span className=\"flex text-rose-600 dark:text-rose-400\">\n            −<Roll value={dels} reduced={reduced} />\n          </span>\n        </motion.div>\n        <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n          <AnimatePresence initial={false}>\n            {review && waiting.length > 0 ? (\n              <motion.div\n                key=\"all\"\n                initial={reduced ? { opacity: 0 } : { opacity: 0, x: 6 }}\n                animate={{ opacity: 1, x: 0 }}\n                exit={{ opacity: 0, transition: TIMING.actionsOut }}\n                transition={TIMING.pill}\n                className=\"flex items-center gap-1\"\n              >\n                <HeaderButton\n                  label=\"Undo all\"\n                  onClick={() => decide(all, \"undone\", \"All changes undone\")}\n                  icon={<UndoIcon />}\n                />\n                <HeaderButton\n                  label=\"Keep all\"\n                  strong\n                  onClick={() => decide(all, \"kept\", \"All changes kept\")}\n                  icon={<CheckIcon />}\n                />\n              </motion.div>\n            ) : null}\n          </AnimatePresence>\n          {replay && finished && count > 0 ? (\n            <button\n              type=\"button\"\n              onClick={start}\n              aria-label=\"Replay the change\"\n              title=\"Replay\"\n              className={`inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors ${TIMING.press} hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/30`}\n            >\n              <svg aria-hidden viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\">\n                <path\n                  d=\"M2.75 8a5.25 5.25 0 1 0 1.6-3.77M2.75 2.5v2.75H5.5\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1.4\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                />\n              </svg>\n            </button>\n          ) : null}\n        </div>\n      </div>\n\n      {/* code */}\n      <div\n        ref={scrollerRef}\n        role=\"region\"\n        aria-label={filename ? `Changes to ${filename}` : \"Changes\"}\n        tabIndex={0}\n        style={{ maxHeight: typeof maxHeight === \"number\" ? `${maxHeight}px` : maxHeight }}\n        className={cn(\n          \"relative outline-none [scrollbar-width:thin] focus-visible:ring-2 focus-visible:ring-foreground/20 focus-visible:ring-inset\",\n          maxHeight != null ? \"overflow-auto\" : wrap ? \"overflow-hidden\" : \"overflow-x-auto overflow-y-hidden\",\n        )}\n      >\n        <pre className=\"sr-only\">{current}</pre>\n        <motion.div\n          key={run.token}\n          ref={contentRef}\n          initial={{ opacity: run.token > 1 && !reduced ? 0 : 1 }}\n          animate={{ opacity: 1 }}\n          transition={TIMING.replay}\n          style={{ minHeight: (reserve || estimate) + 24 }}\n          className={cn(\n            \"relative py-3 font-mono text-[13px] leading-6 [tab-size:2]\",\n            wrap ? \"w-full\" : \"w-max min-w-full\",\n          )}\n        >\n          {model.parts.map((part) => {\n            if (part.kind === \"same\") {\n              const index = part.row.index;\n              return <SameRow key={part.row.key} row={part.row} number={numbers[index]} index={index} {...rowProps} />;\n            }\n            const i = part.index;\n            const hunk = model.hunks[i];\n            const status = statuses[i];\n            return (\n              <div key={`h${i}`} data-hunk={i} className=\"relative\">\n                <AnimatePresence>\n                  {run.active === i && !reduced ? <Band key=\"band\" swapHost={swapBandHost} /> : null}\n                </AnimatePresence>\n                {hunk.dels.length > 0 ? (\n                  <motion.span\n                    aria-hidden\n                    initial={false}\n                    animate={{ opacity: status === \"applied\" ? 1 : 0 }}\n                    transition={TIMING.tint}\n                    className=\"pointer-events-none absolute inset-x-0 top-0 z-[1] h-px bg-rose-500/60 dark:bg-rose-400/60\"\n                  />\n                ) : null}\n                <AnimatePresence>\n                  {review && !inline && status === \"applied\" ? (\n                    <ReviewPill\n                      key=\"pill\"\n                      index={i}\n                      adds={hunk.adds.length}\n                      dels={hunk.dels.length}\n                      onSeam={hunk.adds.length === 0}\n                      reduced={reduced}\n                      onUndo={() => decide([i], \"undone\", `Change ${i + 1} undone`)}\n                      onKeep={() => decide([i], \"kept\", `Change ${i + 1} kept`)}\n                    />\n                  ) : null}\n                </AnimatePresence>\n                {hunk.dels.map((row, k) => (\n                  <ChangedRow\n                    key={row.key}\n                    row={row}\n                    index={hunk.first + k}\n                    number={numbers[hunk.first + k]}\n                    open={!landed(status)}\n                    struck={status === \"striking\" || status === \"applied\"}\n                    tinted={status === \"striking\" || status === \"applied\"}\n                    undoing={status === \"undone\"}\n                    duration={duration}\n                    {...rowProps}\n                  />\n                ))}\n                {hunk.adds.map((row, k) => {\n                  const index = hunk.first + hunk.dels.length + k;\n                  return (\n                    <ChangedRow\n                      key={row.key}\n                      row={row}\n                      index={index}\n                      number={numbers[index]}\n                      open={landed(status)}\n                      struck={false}\n                      tinted={status === \"applied\"}\n                      undoing={status === \"undone\"}\n                      duration={duration}\n                      {...rowProps}\n                    />\n                  );\n                })}\n                {review && inline ? (\n                  <ReviewStrip\n                    index={i}\n                    adds={hunk.adds.length}\n                    dels={hunk.dels.length}\n                    open={status === \"applied\"}\n                    reduced={reduced}\n                    duration={duration}\n                    onUndo={() => decide([i], \"undone\", `Change ${i + 1} undone`)}\n                    onKeep={() => decide([i], \"kept\", `Change ${i + 1} kept`)}\n                  />\n                ) : null}\n              </div>\n            );\n          })}\n        </motion.div>\n      </div>\n\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {run.message}\n      </div>\n    </div>\n  );\n}\n\n/* ── Rows ───────────────────────────────────────────────── */\n\ntype RowShared = {\n  row: Row;\n  index: number;\n  number: number | null;\n  digits: number;\n  lineNumbers: boolean;\n  wrap: boolean;\n  reduced: boolean;\n  wave: number;\n};\n\nconst rollDelay = (index: number, wave: number) => Math.min(TIMING.rollSpread, Math.max(0, index - wave) * TIMING.rollStep);\n\nfunction Code({ tokens }: { tokens: Token[] }) {\n  return (\n    <>\n      {tokens.length === 0 ? \" \" : null}\n      {tokens.map((token, i) => (\n        <span key={i} className={TOKEN_CLASS[token.type]}>\n          {token.text}\n        </span>\n      ))}\n    </>\n  );\n}\n\nfunction Gutter({\n  number,\n  digits,\n  index,\n  wave,\n  reduced,\n  sign,\n  tone,\n}: {\n  number: number | null;\n  digits: number;\n  index: number;\n  wave: number;\n  reduced: boolean;\n  sign: \"+\" | \"−\" | null;\n  tone: React.ReactNode;\n}) {\n  return (\n    <span\n      aria-hidden\n      style={{ width: `calc(${digits}ch + 2rem)` }}\n      className=\"sticky left-0 z-[2] flex shrink-0 justify-end bg-background text-muted-foreground/45 tabular-nums select-none\"\n    >\n      {tone}\n      <span className=\"relative\">\n        <Roll value={number} delay={rollDelay(index, wave)} reduced={reduced} />\n      </span>\n      <span\n        className={cn(\n          \"relative w-5 text-center\",\n          sign === \"+\" ? \"text-emerald-600 dark:text-emerald-400\" : \"text-rose-600 dark:text-rose-400\",\n        )}\n      >\n        {sign}\n      </span>\n    </span>\n  );\n}\n\nconst SameRow = React.memo(function SameRow({ row, index, number, digits, lineNumbers, wrap, reduced, wave }: RowShared) {\n  return (\n    <div data-ver=\"both\" aria-hidden className=\"flex\">\n      {lineNumbers ? (\n        <Gutter number={number} digits={digits} index={index} wave={wave} reduced={reduced} sign={null} tone={null} />\n      ) : (\n        <span className=\"w-4 shrink-0\" />\n      )}\n      <span className={cn(\"min-w-0 flex-1 pr-5\", lineNumbers && \"pl-1\", wrap ? \"break-words whitespace-pre-wrap\" : \"whitespace-pre\")}>\n        <Code tokens={row.tokens} />\n      </span>\n    </div>\n  );\n});\n\nconst ChangedRow = React.memo(function ChangedRow({\n  row,\n  index,\n  number,\n  digits,\n  lineNumbers,\n  wrap,\n  reduced,\n  wave,\n  open,\n  struck,\n  tinted,\n  undoing,\n  duration,\n}: RowShared & { open: boolean; struck: boolean; tinted: boolean; undoing: boolean; duration: number }) {\n  const del = row.kind === \"del\";\n  const fold = reduced ? { duration: 0 } : undoing ? undoOf(duration) : foldOf(duration);\n  const tint = reduced ? { duration: 0 } : TIMING.tint;\n  const rowTint = del ? \"bg-rose-500/[0.09] dark:bg-rose-400/[0.12]\" : \"bg-emerald-500/[0.09] dark:bg-emerald-400/[0.11]\";\n  const gutterTint = del ? \"bg-rose-500/[0.16] dark:bg-rose-400/[0.2]\" : \"bg-emerald-500/[0.16] dark:bg-emerald-400/[0.18]\";\n  return (\n    <motion.div\n      initial={false}\n      animate={{ height: open ? \"auto\" : 0, opacity: open ? 1 : 0 }}\n      transition={{\n        height: fold,\n        opacity: reduced ? { duration: 0 } : open ? TIMING.rowIn : TIMING.rowOut,\n      }}\n      className=\"relative overflow-clip\"\n    >\n      <motion.span\n        aria-hidden\n        initial={false}\n        animate={{ opacity: tinted ? 1 : 0 }}\n        transition={tint}\n        className={cn(\"pointer-events-none absolute inset-0\", rowTint)}\n      />\n      <div data-ver={del ? \"before\" : \"after\"} aria-hidden className=\"relative flex\">\n        {lineNumbers ? (\n          <Gutter\n            number={number}\n            digits={digits}\n            index={index}\n            wave={wave}\n            reduced={reduced}\n            sign={tinted ? (del ? \"−\" : \"+\") : null}\n            tone={\n              <motion.span\n                initial={false}\n                animate={{ opacity: tinted ? 1 : 0 }}\n                transition={tint}\n                className={cn(\"pointer-events-none absolute inset-0\", gutterTint)}\n              />\n            }\n          />\n        ) : (\n          <span className=\"w-4 shrink-0\" />\n        )}\n        <span className={cn(\"relative min-w-0 flex-1 pr-5\", lineNumbers && \"pl-1\", wrap ? \"break-words whitespace-pre-wrap\" : \"whitespace-pre\")}>\n          <span className={cn(`transition-opacity ${TIMING.dim}`, struck && \"opacity-60\")}>\n            <Code tokens={row.tokens} />\n          </span>\n          {del ? (\n            // The strike: a transparent copy of the line with only its line-through\n            // showing, revealed left to right, so wrapped lines strike too.\n            <motion.span\n              aria-hidden\n              initial={false}\n              animate={{ clipPath: struck ? \"inset(0% 0% 0% 0%)\" : \"inset(0% 100% 0% 0%)\" }}\n              transition={reduced ? { duration: 0 } : struck ? TIMING.strike : TIMING.unstrike}\n              className={cn(\n                \"pointer-events-none absolute inset-y-0 left-0 pr-5 text-transparent line-through decoration-rose-500 decoration-[1.5px] dark:decoration-rose-400\",\n                lineNumbers && \"pl-1\",\n                wrap ? \"right-0 whitespace-pre-wrap break-words\" : \"whitespace-pre\",\n              )}\n            >\n              {row.tokens.map((t) => t.text).join(\"\") || \" \"}\n            </motion.span>\n          ) : null}\n        </span>\n      </div>\n    </motion.div>\n  );\n});\n\n/* ── Band ───────────────────────────────────────────────── */\n\n// The soft band behind the hunk being edited. It mounts inside its hunk, so it\n// follows the hunk's height as lines close and open, and flies in from the\n// last hunk it sat on (a flat rectangle, so scaling it can't distort anything).\nfunction Band({ swapHost }: { swapHost: (el: HTMLElement) => HTMLElement | null }) {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  React.useLayoutEffect(() => {\n    const el = ref.current;\n    const mine = el?.parentElement;\n    if (!el || !mine) return;\n    const prev = swapHost(mine);\n    if (!prev || prev === mine || !prev.isConnected) {\n      animate(el, { opacity: [0, 1] }, TIMING.bandIn);\n      return;\n    }\n    const a = prev.getBoundingClientRect();\n    const b = mine.getBoundingClientRect();\n    const fromH = Math.max(a.height, 2);\n    const toH = Math.max(b.height, 2);\n    animate(\n      el,\n      { transform: [`translateY(${a.top - b.top}px) scaleY(${fromH / toH})`, \"translateY(0px) scaleY(1)\"] },\n      TIMING.band,\n    );\n  }, [swapHost]);\n  return (\n    <motion.span\n      ref={ref}\n      aria-hidden\n      exit={{ opacity: 0, transition: TIMING.bandOut }}\n      className=\"pointer-events-none absolute inset-x-0 top-0 bottom-0 z-0 min-h-0.5 origin-top bg-foreground/[0.045] dark:bg-foreground/[0.06]\"\n    >\n      <span className=\"absolute inset-y-0 left-0 w-0.5 bg-foreground/35\" />\n    </motion.span>\n  );\n}\n\n/* ── Review pill ────────────────────────────────────────── */\n\n// Undo / Keep for one hunk. A zero-height strip pinned to the box's visible\n// width keeps it at the right edge however far the code scrolls sideways.\nfunction ReviewPill({\n  index,\n  adds,\n  dels,\n  onSeam,\n  reduced,\n  onUndo,\n  onKeep,\n}: {\n  index: number;\n  adds: number;\n  dels: number;\n  onSeam: boolean;\n  reduced: boolean;\n  onUndo: () => void;\n  onKeep: () => void;\n}) {\n  return (\n    <div className=\"sticky left-0 z-[3] h-0\" style={{ width: \"var(--diff-view, 100%)\" }}>\n      <motion.div\n        initial={reduced ? { opacity: 0 } : { opacity: 0, y: -3, scale: 0.96 }}\n        animate={{ opacity: 1, y: 0, scale: 1 }}\n        exit={{ opacity: 0, scale: 0.97, transition: TIMING.pillOut }}\n        transition={reduced ? TIMING.pillReduced : TIMING.pill}\n        className={cn(\n          \"absolute right-2 flex origin-right items-center gap-0.5 rounded-md border border-border bg-background/95 p-0.5 font-sans shadow-[0_4px_12px_-6px_rgb(0_0_0/0.25)] backdrop-blur-sm\",\n          onSeam ? \"top-0 -translate-y-1/2\" : \"top-0\",\n        )}\n      >\n        <span aria-hidden className=\"hidden px-1.5 font-mono text-[11px] tabular-nums @[26rem]/diff:inline\">\n          {dels ? <span className=\"text-rose-600 dark:text-rose-400\">−{dels}</span> : null}\n          {dels && adds ? \" \" : null}\n          {adds ? <span className=\"text-emerald-600 dark:text-emerald-400\">+{adds}</span> : null}\n        </span>\n        <button\n          type=\"button\"\n          onClick={onUndo}\n          aria-label={`Undo change ${index + 1}`}\n          className={`h-5 cursor-pointer rounded-[5px] px-2 text-[11px] leading-5 font-medium text-muted-foreground outline-none transition-colors ${TIMING.press} hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/30`}\n        >\n          Undo\n        </button>\n        <button\n          type=\"button\"\n          onClick={onKeep}\n          aria-label={`Keep change ${index + 1}`}\n          data-keep={index}\n          className={`h-5 cursor-pointer rounded-[5px] bg-foreground px-2 text-[11px] leading-5 font-medium text-background outline-none transition-opacity ${TIMING.press} hover:opacity-85 focus-visible:ring-2 focus-visible:ring-foreground/30 focus-visible:ring-offset-1 focus-visible:ring-offset-background`}\n        >\n          Keep\n        </button>\n      </motion.div>\n    </div>\n  );\n}\n\n// The same choice as a strip that opens under the change, for prose and\n// narrow boxes. It stays mounted, closed and inert, so the box can reserve\n// its height before the change plays.\nfunction ReviewStrip({\n  index,\n  adds,\n  dels,\n  open,\n  reduced,\n  duration,\n  onUndo,\n  onKeep,\n}: {\n  index: number;\n  adds: number;\n  dels: number;\n  open: boolean;\n  reduced: boolean;\n  duration: number;\n  onUndo: () => void;\n  onKeep: () => void;\n}) {\n  return (\n    <motion.div\n      initial={false}\n      animate={{ height: open ? \"auto\" : 0, opacity: open ? 1 : 0 }}\n      transition={\n        reduced\n          ? { duration: 0 }\n          : { height: open ? foldOf(duration) : undoOf(duration), opacity: { duration: open ? TIMING.stripFade.open : TIMING.stripFade.close, ease: TIMING.stripFade.ease } }\n      }\n      inert={!open}\n      className=\"relative overflow-clip\"\n    >\n      <div\n        data-ver=\"after\"\n        className=\"border-b border-emerald-500/15 bg-emerald-500/[0.045] dark:border-emerald-400/15 dark:bg-emerald-400/[0.06]\"\n      >\n        {/* Pinned to the box's visible width, so the buttons never scroll away with long lines. */}\n        <div\n          style={{ width: \"var(--diff-view, 100%)\" }}\n          className=\"sticky left-0 flex items-center gap-1 py-1 pr-2 pl-3 font-sans\"\n        >\n        <span aria-hidden className=\"mr-auto font-mono text-[11px] text-muted-foreground tabular-nums\">\n          {dels ? <span className=\"text-rose-600 dark:text-rose-400\">−{dels}</span> : null}\n          {dels && adds ? \" \" : null}\n          {adds ? <span className=\"text-emerald-600 dark:text-emerald-400\">+{adds}</span> : null}\n        </span>\n        <button\n          type=\"button\"\n          onClick={onUndo}\n          aria-label={`Undo change ${index + 1}`}\n          className={`h-7 cursor-pointer rounded-md px-2.5 text-[12px] font-medium text-muted-foreground outline-none transition-colors ${TIMING.press} hover:bg-foreground/5 hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/30`}\n        >\n          Undo\n        </button>\n        <button\n          type=\"button\"\n          onClick={onKeep}\n          aria-label={`Keep change ${index + 1}`}\n          data-keep={index}\n          className={`h-7 cursor-pointer rounded-md bg-foreground px-2.5 text-[12px] font-medium text-background outline-none transition-opacity ${TIMING.press} hover:opacity-85 focus-visible:ring-2 focus-visible:ring-foreground/30 focus-visible:ring-offset-1 focus-visible:ring-offset-background`}\n        >\n          Keep\n        </button>\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n\n/* ── Header bits ────────────────────────────────────────── */\n\nfunction HeaderButton({ label, onClick, icon, strong }: { label: string; onClick: () => void; icon: React.ReactNode; strong?: boolean }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      aria-label={label}\n      className={cn(\n        `inline-flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-[12px] font-medium outline-none transition-[background-color,color,opacity] ${TIMING.press} focus-visible:ring-2 focus-visible:ring-foreground/30`,\n        strong ? \"bg-foreground text-background hover:opacity-85\" : \"text-muted-foreground hover:bg-foreground/5 hover:text-foreground\",\n      )}\n    >\n      <span className=\"size-3.5\">{icon}</span>\n      <span className=\"hidden @[30rem]/diff:inline\">{label}</span>\n    </button>\n  );\n}\n\nfunction UndoIcon() {\n  return (\n    <svg aria-hidden viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\">\n      <path d=\"M5.5 4 2.5 7l3 3\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      <path d=\"M3 7h6.25a4 4 0 0 1 0 8H7\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nfunction CheckIcon() {\n  return (\n    <svg aria-hidden viewBox=\"0 0 16 16\" className=\"size-3.5\" fill=\"none\">\n      <path d=\"m3.25 8.5 3 3 6.5-7\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\n/* ── Roll ───────────────────────────────────────────────── */\n\n// A number that rolls to its new value: up when it grows, down when it\n// shrinks. Holds its last value while it has none (a line that's closing).\nfunction Roll({ value, delay = 0, reduced }: { value: number | null; delay?: number; reduced: boolean }) {\n  const [shown, setShown] = React.useState({ n: value, dir: 1 });\n  if (value != null && value !== shown.n) setShown({ n: value, dir: value > (shown.n ?? 0) ? 1 : -1 });\n  const transition = reduced ? { duration: 0 } : { ...TIMING.roll, delay };\n  return (\n    // A block-level grid: an inline one sits on the baseline and adds a few\n    // pixels under every line while a number rolls.\n    <span className=\"relative grid overflow-hidden\">\n      <AnimatePresence initial={false} custom={shown.dir}>\n        <motion.span\n          key={shown.n ?? \"none\"}\n          custom={shown.dir}\n          variants={{\n            enter: (dir: number) => ({ y: dir > 0 ? \"75%\" : \"-75%\", opacity: 0 }),\n            center: { y: \"0%\", opacity: 1 },\n            exit: (dir: number) => ({ y: dir > 0 ? \"-75%\" : \"75%\", opacity: 0 }),\n          }}\n          initial=\"enter\"\n          animate=\"center\"\n          exit=\"exit\"\n          transition={transition}\n          className=\"col-start-1 row-start-1\"\n        >\n          {shown.n ?? \"\\u00a0\"}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n"
    }
  ]
}