{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-view",
  "title": "Diff View",
  "description": "A line-by-line diff of two strings — the before/after view a screen needs when it has to show what changed: a config or settings change, a record edited in an admin panel, a document revision, a webhook payload against the last one, an audit-log entry, a restored backup next to what is live, or the edit an AI agent is proposing before the user accepts it. Pass before and after and it renders a git-style diff, unified by default or side by side with view=\"split\". Unchanged lines collapse into a counted gap, so a 400-line file with a three-line change shows three lines and a summary instead of 400; context sets how many surrounding lines survive and context={Infinity} shows the whole text. Both line-number gutters are select-none, so selecting the diff copies the code and not a column of numbers. CRLF and LF are folded together, because a file that changed only its line endings would otherwise report every single line as rewritten. It is meaning-first rather than colour-first: every changed row carries a + or - sign and a screen-reader-only \"Added line:\" / \"Removed line:\" prefix, so the diff still reads for someone who cannot tell the red and green backgrounds apart — conveying the change by colour alone, which fails WCAG 1.4.1, is the single most common defect in a hand-rolled diff. The table also gets an sr-only caption stating how many lines were added and removed. The diff is a longest-common-subsequence over lines with the shared prefix and suffix trimmed off first, which keeps a large document with a small edit fast (a 4,000-line file with one changed line diffs in well under a millisecond) and makes an appended line read as appended instead of shifting everything by one. Pathologically large inputs degrade to \"this block was replaced\" rather than allocating a table of hundreds of megabytes during a render. No dependencies, no diff library and no hooks, so it renders inside a React server component without a \"use client\" of its own and ships no client JavaScript — which is the common case, because the text being compared has usually just been fetched on the server. Official shadcn/ui has no diff component of any kind: table is an unstyled table and chart is a Recharts wrapper, and neither computes or displays a change.",
  "files": [
    {
      "path": "registry/ui/diff-view.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype ChangeType = \"equal\" | \"insert\" | \"delete\"\n\ninterface Op {\n  type: ChangeType\n  /** 1-based line number in `before`, or 0 for an inserted line that has none. */\n  a: number\n  /** 1-based line number in `after`, or 0 for a deleted line that has none. */\n  b: number\n  text: string\n}\n\ntype Row = Op | { type: \"gap\"; count: number }\n\n/**\n * A diff of two texts is a diff of their lines, so how the text is cut into\n * lines decides the whole result. `\\r\\n` is folded into `\\n` rather than kept,\n * because a file that changed line endings would otherwise report every single\n * line as rewritten — technically true and useless to look at.\n *\n * A trailing newline yields a final empty element that is an artefact of the\n * split rather than a line anyone wrote, so one of them is dropped. Two\n * trailing newlines mean there really is a blank last line, and that one stays.\n */\nfunction splitLines(text: string): string[] {\n  const source = String(text ?? \"\")\n  // An empty text has no lines at all. Splitting it yields [\"\"], which is a blank line that\n  // nobody wrote, and the diff then reports it as removed the first time a field is filled in\n  // — so an empty-to-filled change, which is most of what an admin panel shows a diff of,\n  // came out as \"1 line added, 1 line removed\" with a phantom \"Removed line:\" row.\n  if (source === \"\") return []\n  const lines = source.split(/\\r?\\n/)\n  if (lines.length > 1 && lines[lines.length - 1] === \"\") lines.pop()\n  return lines\n}\n\n/**\n * The LCS table is the one part of this component that can grow without bound:\n * it holds one cell per pair of lines, so two 5,000-line files would ask for 25\n * million of them. Past this many cells the diff degrades to \"everything was\n * replaced\" instead of allocating hundreds of megabytes inside a render.\n *\n * A million cells is roughly a 1,000-line file against another, which covers\n * the config files, records and generated documents this is pointed at, and\n * costs 4MB in the typed array below.\n */\nconst MAX_CELLS = 1_000_000\n\n/**\n * Longest common subsequence over lines, with the two cheap wins applied first:\n * a shared prefix and a shared suffix are equal by inspection, so they never\n * enter the table. This is what keeps the usual case — a large document with a\n * small edit in the middle — linear instead of quadratic.\n */\nfunction diffLines(before: string[], after: string[]): Op[] {\n  const ops: Op[] = []\n\n  let start = 0\n  while (start < before.length && start < after.length && before[start] === after[start]) {\n    ops.push({ type: \"equal\", a: start + 1, b: start + 1, text: before[start] })\n    start++\n  }\n\n  let endA = before.length\n  let endB = after.length\n  while (endA > start && endB > start && before[endA - 1] === after[endB - 1]) {\n    endA--\n    endB--\n  }\n\n  const midA = before.slice(start, endA)\n  const midB = after.slice(start, endB)\n  const n = midA.length\n  const m = midB.length\n\n  const tail: Op[] = []\n  for (let k = endA; k < before.length; k++) {\n    tail.push({ type: \"equal\", a: k + 1, b: k - endA + endB + 1, text: before[k] })\n  }\n\n  if (n === 0 || m === 0 || (n + 1) * (m + 1) > MAX_CELLS) {\n    // One side is empty, or the table would be too large to build: report the\n    // middle as wholly removed and wholly added. Still a correct diff, just a\n    // coarser one than the table would have produced.\n    for (let i = 0; i < n; i++) ops.push({ type: \"delete\", a: start + i + 1, b: 0, text: midA[i] })\n    for (let j = 0; j < m; j++) ops.push({ type: \"insert\", a: 0, b: start + j + 1, text: midB[j] })\n    return ops.concat(tail)\n  }\n\n  // dp[i][j] = length of the LCS of midA[i:] and midB[j:]. Suffix-indexed so the\n  // walk below runs forward, which is the order the rows are rendered in.\n  const w = m + 1\n  const dp = new Uint32Array((n + 1) * w)\n  for (let i = n - 1; i >= 0; i--) {\n    for (let j = m - 1; j >= 0; j--) {\n      dp[i * w + j] =\n        midA[i] === midB[j]\n          ? dp[(i + 1) * w + j + 1] + 1\n          : Math.max(dp[(i + 1) * w + j], dp[i * w + j + 1])\n    }\n  }\n\n  let i = 0\n  let j = 0\n  while (i < n && j < m) {\n    if (midA[i] === midB[j]) {\n      ops.push({ type: \"equal\", a: start + i + 1, b: start + j + 1, text: midA[i] })\n      i++\n      j++\n    } else if (dp[(i + 1) * w + j] >= dp[i * w + j + 1]) {\n      // Ties go to the deletion so that a replaced block reads as its removed\n      // lines followed by its added ones, which is what the split view pairs up.\n      ops.push({ type: \"delete\", a: start + i + 1, b: 0, text: midA[i] })\n      i++\n    } else {\n      ops.push({ type: \"insert\", a: 0, b: start + j + 1, text: midB[j] })\n      j++\n    }\n  }\n  while (i < n) {\n    ops.push({ type: \"delete\", a: start + i + 1, b: 0, text: midA[i] })\n    i++\n  }\n  while (j < m) {\n    ops.push({ type: \"insert\", a: 0, b: start + j + 1, text: midB[j] })\n    j++\n  }\n\n  return ops.concat(tail)\n}\n\n/**\n * Unchanged lines far from any edit are noise, so only `context` of them either\n * side of a change survive; the rest collapse into a single counted gap.\n *\n * The neighbourhoods are marked before anything is collapsed, so two edits close\n * enough for their context to touch keep the lines between them and read as one\n * hunk. Collapsing each edit separately instead would cut a gap of \"1 unchanged\n * line\" into the middle of a change that a reader wants to see whole.\n */\nfunction collapse(ops: Op[], context: number): Row[] {\n  // How far each line is from the nearest change, in two passes.\n  //\n  // Walking a window outwards from every change instead would be quadratic when `context` is\n  // large — a whole-file deletion with a generous context is exactly the shape that hurts — and\n  // guarding that with \"show everything if context >= line count\" quietly changes the answer for\n  // a text with no changes at all. Distances have neither problem, and they give `Infinity` its\n  // literal meaning: with nothing changed every line is infinitely far away, which only an\n  // infinite context takes in.\n  const pad = Number.isFinite(context) ? Math.max(0, Math.floor(context)) : context\n  const n = ops.length\n  const dist = new Array<number>(n).fill(Infinity)\n\n  let d = Infinity\n  for (let i = 0; i < n; i++) {\n    if (ops[i].type !== \"equal\") d = 0\n    else if (d !== Infinity) d++\n    dist[i] = d\n  }\n  d = Infinity\n  for (let i = n - 1; i >= 0; i--) {\n    if (ops[i].type !== \"equal\") d = 0\n    else if (d !== Infinity) d++\n    if (d < dist[i]) dist[i] = d\n  }\n\n  const rows: Row[] = []\n  let hidden = 0\n  for (let i = 0; i < n; i++) {\n    if (dist[i] <= pad) {\n      if (hidden > 0) {\n        rows.push({ type: \"gap\", count: hidden })\n        hidden = 0\n      }\n      rows.push(ops[i])\n    } else {\n      hidden++\n    }\n  }\n  if (hidden > 0) rows.push({ type: \"gap\", count: hidden })\n  return rows\n}\n\ninterface Pair {\n  left: Op | null\n  right: Op | null\n}\n\n/**\n * Side-by-side pairing. Deletions and insertions accumulate until the run ends,\n * then line up index by index so a rewritten line sits opposite the line it\n * replaced; whichever side is shorter is padded with blanks.\n */\nfunction pairRows(rows: Row[]): Array<Pair | { type: \"gap\"; count: number }> {\n  const out: Array<Pair | { type: \"gap\"; count: number }> = []\n  let dels: Op[] = []\n  let inss: Op[] = []\n\n  const flush = () => {\n    for (let i = 0; i < Math.max(dels.length, inss.length); i++) {\n      out.push({ left: dels[i] ?? null, right: inss[i] ?? null })\n    }\n    dels = []\n    inss = []\n  }\n\n  for (const row of rows) {\n    if (row.type === \"delete\") dels.push(row)\n    else if (row.type === \"insert\") inss.push(row)\n    else {\n      flush()\n      if (row.type === \"gap\") out.push(row)\n      else out.push({ left: row, right: row })\n    }\n  }\n  flush()\n  return out\n}\n\n/** Colour is not the only carrier of meaning here — see the sign column below. */\nconst TONE: Record<ChangeType, string> = {\n  equal: \"\",\n  // shadcn/ui ships no \"success\" token, so an addition borrows the same emerald\n  // pair used elsewhere in this registry, with an explicit dark-mode value.\n  insert: \"bg-emerald-500/10\",\n  delete: \"bg-destructive/10\",\n}\n\nconst SIGN_TONE: Record<ChangeType, string> = {\n  equal: \"text-muted-foreground/50\",\n  insert: \"text-emerald-600 dark:text-emerald-400\",\n  delete: \"text-destructive\",\n}\n\nconst SIGN: Record<ChangeType, string> = { equal: \" \", insert: \"+\", delete: \"-\" }\n\n/** Spoken before the line itself, so the change is not carried by colour alone. */\nconst SPOKEN: Record<ChangeType, string> = {\n  equal: \"\",\n  insert: \"Added line: \",\n  delete: \"Removed line: \",\n}\n\ninterface DiffViewProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  /** The original text. */\n  before: string\n  /** The changed text. */\n  after: string\n  /** `unified` stacks one column; `split` shows before and after side by side. */\n  view?: \"unified\" | \"split\"\n  /**\n   * Unchanged lines to keep either side of a change. `Infinity` shows the whole\n   * text. Collapsed runs are summarised, not expandable — this component holds\n   * no state; pass `Infinity` when the reader needs everything.\n   */\n  context?: number\n  /** Show the before/after line-number gutters. */\n  lineNumbers?: boolean\n  /** Column headers, used by the split view and the accessible summary. */\n  beforeLabel?: string\n  afterLabel?: string\n  /** Rendered in place of the table when the two texts are identical. */\n  emptyMessage?: React.ReactNode\n  /** Accessible name for the table. */\n  label?: string\n}\n\n/**\n * Line-level diff of two texts.\n *\n * Deliberately free of hooks and state, so it renders in a server component\n * with no client JavaScript — the common case is showing a change that was\n * already fetched on the server. The cost is that the diff recomputes on every\n * render: memoise `before`/`after` upstream if they change on a hot path.\n */\nexport function DiffView({\n  before,\n  after,\n  view = \"unified\",\n  context = 3,\n  lineNumbers = true,\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  emptyMessage = \"No changes.\",\n  label = \"Differences\",\n  className,\n  ...props\n}: DiffViewProps) {\n  const ops = diffLines(splitLines(before), splitLines(after))\n  const added = ops.reduce((n, o) => n + (o.type === \"insert\" ? 1 : 0), 0)\n  const removed = ops.reduce((n, o) => n + (o.type === \"delete\" ? 1 : 0), 0)\n\n  if (added === 0 && removed === 0) {\n    return (\n      <div\n        className={cn(\n          \"rounded-md border bg-muted/30 p-4 text-center text-sm text-muted-foreground\",\n          className\n        )}\n        {...props}\n      >\n        {emptyMessage}\n      </div>\n    )\n  }\n\n  const rows = collapse(ops, context)\n  const summary = `${label}: ${added} line${added === 1 ? \"\" : \"s\"} added, ${removed} line${\n    removed === 1 ? \"\" : \"s\"\n  } removed.`\n\n  const gutter =\n    \"w-[1%] select-none whitespace-nowrap px-2 text-right align-top text-xs tabular-nums text-muted-foreground/70\"\n  const cell = \"whitespace-pre-wrap break-words px-2 align-top\"\n\n  return (\n    <div\n      className={cn(\"overflow-hidden rounded-md border font-mono text-sm\", className)}\n      {...props}\n    >\n      <table className=\"w-full border-collapse\">\n        <caption className=\"sr-only\">{summary}</caption>\n        {view === \"split\" ? (\n          <SplitBody\n            rows={rows}\n            lineNumbers={lineNumbers}\n            beforeLabel={beforeLabel}\n            afterLabel={afterLabel}\n            gutter={gutter}\n            cell={cell}\n          />\n        ) : (\n          <UnifiedBody rows={rows} lineNumbers={lineNumbers} gutter={gutter} cell={cell} />\n        )}\n      </table>\n    </div>\n  )\n}\n\n/**\n * The collapsed-run marker. `colSpan` is deliberately generous: it only has to\n * cover the widest row the table can produce, and an over-wide span is ignored.\n */\nfunction Gap({ count }: { count: number }) {\n  return (\n    <tr className=\"bg-muted/40 text-muted-foreground\">\n      <td colSpan={6} className=\"px-2 py-1 text-center text-xs\">\n        ⋯ {count} unchanged line{count === 1 ? \"\" : \"s\"}\n      </td>\n    </tr>\n  )\n}\n\nfunction UnifiedBody({\n  rows,\n  lineNumbers,\n  gutter,\n  cell,\n}: {\n  rows: Row[]\n  lineNumbers: boolean\n  gutter: string\n  cell: string\n}) {\n  return (\n    <tbody>\n      {rows.map((row, i) =>\n        row.type === \"gap\" ? (\n          <Gap key={`gap-${i}`} count={row.count} />\n        ) : (\n          <tr key={`${row.type}-${row.a}-${row.b}-${i}`} className={TONE[row.type]}>\n            {lineNumbers && (\n              <>\n                <td className={gutter} aria-hidden=\"true\">\n                  {row.a || \"\"}\n                </td>\n                <td className={gutter} aria-hidden=\"true\">\n                  {row.b || \"\"}\n                </td>\n              </>\n            )}\n            <td\n              className={cn(\"w-[1%] select-none px-1 text-center align-top\", SIGN_TONE[row.type])}\n              aria-hidden=\"true\"\n            >\n              {SIGN[row.type]}\n            </td>\n            <td className={cell}>\n              <span className=\"sr-only\">{SPOKEN[row.type]}</span>\n              {row.text || \" \"}\n            </td>\n          </tr>\n        )\n      )}\n    </tbody>\n  )\n}\n\nfunction SplitBody({\n  rows,\n  lineNumbers,\n  beforeLabel,\n  afterLabel,\n  gutter,\n  cell,\n}: {\n  rows: Row[]\n  lineNumbers: boolean\n  beforeLabel: string\n  afterLabel: string\n  gutter: string\n  cell: string\n}) {\n  const pairs = pairRows(rows)\n  return (\n    <>\n      <thead>\n        <tr className=\"border-b bg-muted/50 text-xs text-muted-foreground\">\n          <th scope=\"col\" colSpan={lineNumbers ? 2 : 1} className=\"px-2 py-1 text-left font-medium\">\n            {beforeLabel}\n          </th>\n          <th scope=\"col\" colSpan={lineNumbers ? 2 : 1} className=\"px-2 py-1 text-left font-medium\">\n            {afterLabel}\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {pairs.map((row, i) =>\n          \"type\" in row && row.type === \"gap\" ? (\n            <Gap key={`gap-${i}`} count={row.count} />\n          ) : (\n            <tr key={`pair-${i}`}>\n              <Side op={(row as Pair).left} lineNumbers={lineNumbers} gutter={gutter} cell={cell} />\n              <Side\n                op={(row as Pair).right}\n                lineNumbers={lineNumbers}\n                gutter={gutter}\n                cell={cell}\n                divider\n              />\n            </tr>\n          )\n        )}\n      </tbody>\n    </>\n  )\n}\n\n/**\n * One half of a split row. A `null` op is the padding opposite a run of a\n * different length; it is empty rather than absent so the two columns stay in\n * step, and it is hidden from assistive technology because there is no line\n * there to read.\n */\nfunction Side({\n  op,\n  lineNumbers,\n  gutter,\n  cell,\n  divider,\n}: {\n  op: Op | null\n  lineNumbers: boolean\n  gutter: string\n  cell: string\n  divider?: boolean\n}) {\n  const tone = op ? TONE[op.type] : \"bg-muted/20\"\n  const edge = divider ? \"border-l\" : \"\"\n  if (!op) {\n    return (\n      <td className={cn(cell, tone, edge)} colSpan={lineNumbers ? 2 : 1} aria-hidden=\"true\">\n        {\" \"}\n      </td>\n    )\n  }\n  return (\n    <>\n      {lineNumbers && (\n        <td className={cn(gutter, tone, edge)} aria-hidden=\"true\">\n          {(op.type === \"insert\" ? op.b : op.a) || \"\"}\n        </td>\n      )}\n      <td className={cn(cell, tone, !lineNumbers && edge)}>\n        <span className=\"sr-only\">{SPOKEN[op.type]}</span>\n        <span className={cn(\"select-none pr-1\", SIGN_TONE[op.type])} aria-hidden=\"true\">\n          {SIGN[op.type]}\n        </span>\n        {op.text || \" \"}\n      </td>\n    </>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}