{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-view",
  "title": "Diff View",
  "description": "A proposed change shown as a unified or side-by-side diff, with optional accept and reject controls.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/diff-view/diff-view.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, FileDiff, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type DiffLineKind = \"context\" | \"add\" | \"remove\"\n\nexport type DiffLine = {\n  kind: DiffLineKind\n  text: string\n  beforeNumber?: number\n  afterNumber?: number\n}\n\nexport type DiffHunk = {\n  header?: string\n  lines: readonly DiffLine[]\n}\n\nexport type DiffViewProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  before?: string\n  after?: string\n  /** Precomputed hunks. Supplied hunks win over before and after. */\n  hunks?: readonly DiffHunk[]\n  filename?: string\n  view?: \"unified\" | \"split\"\n  /** Unchanged lines kept either side of a change. Defaults to 3. */\n  context?: number\n  showLineNumbers?: boolean\n  onAccept?: () => void\n  onReject?: () => void\n  acceptLabel?: string\n  rejectLabel?: string\n  status?: \"pending\" | \"accepted\" | \"rejected\"\n}\n\n/**\n * Past this many cells the quadratic table costs more than the result is\n * worth, so the two sides are reported as one wholesale replacement instead.\n */\nconst MAX_CELLS = 2_000_000\n\nfunction splitLines(value: string) {\n  return value === \"\" ? [] : value.replace(/\\n$/, \"\").split(\"\\n\")\n}\n\n/**\n * A line-level diff. Common prefixes and suffixes are peeled off first, which\n * is what keeps an edit to one line of a long file cheap, and the remaining\n * middles go through a longest-common-subsequence table.\n */\nexport function diffLines(before: string, after: string): DiffLine[] {\n  const left = splitLines(before)\n  const right = splitLines(after)\n\n  let head = 0\n  while (\n    head < left.length &&\n    head < right.length &&\n    left[head] === right[head]\n  ) {\n    head++\n  }\n\n  let tail = 0\n  while (\n    tail < left.length - head &&\n    tail < right.length - head &&\n    left[left.length - 1 - tail] === right[right.length - 1 - tail]\n  ) {\n    tail++\n  }\n\n  const leftMiddle = left.slice(head, left.length - tail)\n  const rightMiddle = right.slice(head, right.length - tail)\n\n  const lines: DiffLine[] = []\n  let beforeNumber = 1\n  let afterNumber = 1\n\n  for (let i = 0; i < head; i++) {\n    lines.push({\n      kind: \"context\",\n      text: left[i]!,\n      beforeNumber: beforeNumber++,\n      afterNumber: afterNumber++,\n    })\n  }\n\n  const middle =\n    leftMiddle.length * rightMiddle.length > MAX_CELLS\n      ? [\n          ...leftMiddle.map((text) => ({ kind: \"remove\" as const, text })),\n          ...rightMiddle.map((text) => ({ kind: \"add\" as const, text })),\n        ]\n      : commonSubsequenceDiff(leftMiddle, rightMiddle)\n\n  for (const line of middle) {\n    lines.push({\n      ...line,\n      beforeNumber: line.kind === \"add\" ? undefined : beforeNumber++,\n      afterNumber: line.kind === \"remove\" ? undefined : afterNumber++,\n    })\n  }\n\n  for (let i = left.length - tail; i < left.length; i++) {\n    lines.push({\n      kind: \"context\",\n      text: left[i]!,\n      beforeNumber: beforeNumber++,\n      afterNumber: afterNumber++,\n    })\n  }\n\n  return lines\n}\n\nfunction commonSubsequenceDiff(\n  left: readonly string[],\n  right: readonly string[]\n) {\n  const rows = left.length\n  const columns = right.length\n  const table: number[][] = Array.from({ length: rows + 1 }, () =>\n    new Array<number>(columns + 1).fill(0)\n  )\n\n  for (let i = rows - 1; i >= 0; i--) {\n    for (let j = columns - 1; j >= 0; j--) {\n      table[i]![j] =\n        left[i] === right[j]\n          ? table[i + 1]![j + 1]! + 1\n          : Math.max(table[i + 1]![j]!, table[i]![j + 1]!)\n    }\n  }\n\n  const lines: { kind: DiffLineKind; text: string }[] = []\n  let i = 0\n  let j = 0\n\n  while (i < rows && j < columns) {\n    if (left[i] === right[j]) {\n      lines.push({ kind: \"context\", text: left[i]! })\n      i++\n      j++\n    } else if (table[i + 1]![j]! >= table[i]![j + 1]!) {\n      lines.push({ kind: \"remove\", text: left[i]! })\n      i++\n    } else {\n      lines.push({ kind: \"add\", text: right[j]! })\n      j++\n    }\n  }\n\n  while (i < rows) lines.push({ kind: \"remove\", text: left[i++]! })\n  while (j < columns) lines.push({ kind: \"add\", text: right[j++]! })\n\n  return lines\n}\n\n/** Drops runs of unchanged lines longer than twice the context window. */\nexport function toHunks(\n  lines: readonly DiffLine[],\n  context: number\n): DiffHunk[] {\n  const changed = lines\n    .map((line, index) => (line.kind === \"context\" ? -1 : index))\n    .filter((index) => index >= 0)\n\n  if (changed.length === 0) return []\n\n  const ranges: [number, number][] = []\n\n  for (const index of changed) {\n    const start = Math.max(0, index - context)\n    const end = Math.min(lines.length - 1, index + context)\n    const last = ranges[ranges.length - 1]\n\n    if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end)\n    else ranges.push([start, end])\n  }\n\n  return ranges.map(([start, end]) => {\n    const slice = lines.slice(start, end + 1)\n    const first = slice[0]!\n\n    return {\n      header: `@@ -${first.beforeNumber ?? 1} +${first.afterNumber ?? 1} @@`,\n      lines: slice,\n    }\n  })\n}\n\ntype Row = { left?: DiffLine; right?: DiffLine }\n\n/** Pairs each removal with the addition that replaced it, for the split view. */\nfunction toRows(lines: readonly DiffLine[]): Row[] {\n  const rows: Row[] = []\n  let removed: DiffLine[] = []\n\n  const flush = () => {\n    for (const line of removed) rows.push({ left: line })\n    removed = []\n  }\n\n  for (const line of lines) {\n    if (line.kind === \"remove\") {\n      removed.push(line)\n    } else if (line.kind === \"add\") {\n      rows.push({ left: removed.shift(), right: line })\n    } else {\n      flush()\n      rows.push({ left: line, right: line })\n    }\n  }\n\n  flush()\n\n  return rows\n}\n\nconst tone: Record<DiffLineKind, string> = {\n  add: \"bg-[color-mix(in_oklab,var(--accent)_18%,transparent)]\",\n  remove: \"bg-[color-mix(in_oklab,var(--destructive)_14%,transparent)]\",\n  context: \"\",\n}\n\nconst sign: Record<DiffLineKind, string> = {\n  add: \"+\",\n  remove: \"-\",\n  context: \" \",\n}\n\nexport function DiffView({\n  before = \"\",\n  after = \"\",\n  hunks,\n  filename,\n  view = \"unified\",\n  context = 3,\n  showLineNumbers = true,\n  onAccept,\n  onReject,\n  acceptLabel = \"Accept\",\n  rejectLabel = \"Reject\",\n  status = \"pending\",\n  className,\n  ...rootProps\n}: DiffViewProps) {\n  const resolved = React.useMemo(\n    () => hunks ?? toHunks(diffLines(before, after), context),\n    [hunks, before, after, context]\n  )\n\n  const counts = React.useMemo(() => {\n    let added = 0\n    let removed = 0\n\n    for (const hunk of resolved) {\n      for (const line of hunk.lines) {\n        if (line.kind === \"add\") added++\n        if (line.kind === \"remove\") removed++\n      }\n    }\n\n    return { added, removed }\n  }, [resolved])\n\n  const gutter = React.useMemo(() => {\n    const highest = resolved.flatMap((hunk) =>\n      hunk.lines.map((line) =>\n        Math.max(line.beforeNumber ?? 0, line.afterNumber ?? 0)\n      )\n    )\n\n    return String(Math.max(1, ...highest)).length\n  }, [resolved])\n\n  const decided = status !== \"pending\"\n\n  return (\n    <div\n      data-slot=\"diff-view\"\n      data-status={status}\n      className={cn(\n        \"border-border bg-muted/40 min-w-0 overflow-hidden rounded-[calc(var(--radius)+0.15rem)] border\",\n        className\n      )}\n      {...rootProps}\n    >\n      <div\n        data-slot=\"diff-view-bar\"\n        className=\"border-border flex items-center gap-2 border-b px-3 py-1.5\"\n      >\n        <FileDiff\n          aria-hidden=\"true\"\n          size={13}\n          className=\"text-muted-foreground shrink-0\"\n        />\n        <span className=\"text-foreground min-w-0 truncate font-[family-name:var(--font-mono),monospace] text-xs\">\n          {filename ?? \"Proposed change\"}\n        </span>\n\n        <span className=\"ml-auto shrink-0 font-[family-name:var(--font-mono),monospace] text-[0.6875rem]\">\n          <span className=\"text-[color-mix(in_oklab,var(--accent)_75%,var(--foreground))]\">\n            +{counts.added}\n          </span>{\" \"}\n          <span className=\"text-destructive\">-{counts.removed}</span>\n        </span>\n      </div>\n\n      {resolved.length === 0 ? (\n        <p className=\"text-muted-foreground px-3 py-4 text-xs\">No changes.</p>\n      ) : (\n        <div className=\"overflow-x-auto\">\n          <table className=\"w-full border-collapse font-[family-name:var(--font-mono),monospace] text-xs leading-relaxed\">\n            <caption className=\"sr-only\">\n              {filename ? `Changes to ${filename}` : \"Proposed change\"}:{\" \"}\n              {counts.added} added, {counts.removed} removed\n            </caption>\n            <tbody>\n              {resolved.map((hunk, hunkIndex) => (\n                <React.Fragment key={hunkIndex}>\n                  {hunk.header ? (\n                    <tr>\n                      <td\n                        colSpan={view === \"split\" ? 4 : 2}\n                        className=\"border-border text-muted-foreground bg-muted/60 border-y px-3 py-1 text-[0.6875rem]\"\n                      >\n                        {hunk.header}\n                      </td>\n                    </tr>\n                  ) : null}\n\n                  {view === \"split\"\n                    ? toRows(hunk.lines).map((row, index) => (\n                        <tr key={index}>\n                          <Side\n                            line={row.left}\n                            side=\"before\"\n                            gutter={gutter}\n                            numbered={showLineNumbers}\n                          />\n                          <Side\n                            line={row.right}\n                            side=\"after\"\n                            gutter={gutter}\n                            numbered={showLineNumbers}\n                          />\n                        </tr>\n                      ))\n                    : hunk.lines.map((line, index) => (\n                        <tr key={index} className={tone[line.kind]}>\n                          {showLineNumbers ? (\n                            <td\n                              aria-hidden=\"true\"\n                              className=\"text-muted-foreground/60 w-px pl-3 text-right align-top tabular-nums select-none\"\n                              style={{ minWidth: `${gutter}ch` }}\n                            >\n                              {line.afterNumber ?? line.beforeNumber}\n                            </td>\n                          ) : null}\n                          <td className=\"w-full px-3 align-top\">\n                            <span\n                              aria-hidden=\"true\"\n                              className=\"text-muted-foreground/70 select-none\"\n                            >\n                              {sign[line.kind]}{\" \"}\n                            </span>\n                            <span className=\"break-words whitespace-pre-wrap\">\n                              {line.text === \"\" ? \" \" : line.text}\n                            </span>\n                          </td>\n                        </tr>\n                      ))}\n                </React.Fragment>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      )}\n\n      {onAccept || onReject ? (\n        <div\n          data-slot=\"diff-view-decision\"\n          className=\"border-border flex items-center gap-2 border-t px-3 py-2\"\n        >\n          {decided ? (\n            <p className=\"text-muted-foreground text-xs\" role=\"status\">\n              {status === \"accepted\" ? \"Change accepted.\" : \"Change rejected.\"}\n            </p>\n          ) : (\n            <>\n              {onReject ? (\n                <button\n                  type=\"button\"\n                  className=\"border-border text-muted-foreground hover:text-foreground hover:bg-card min-h-8 rounded-md border px-3 text-xs transition-colors duration-150 motion-reduce:transition-none\"\n                  onClick={onReject}\n                >\n                  <X aria-hidden=\"true\" size={12} className=\"mr-1 inline\" />\n                  {rejectLabel}\n                </button>\n              ) : null}\n              {onAccept ? (\n                <button\n                  type=\"button\"\n                  className=\"bg-primary text-primary-foreground min-h-8 rounded-md px-3 text-xs transition-opacity duration-150 hover:opacity-90 motion-reduce:transition-none\"\n                  onClick={onAccept}\n                >\n                  <Check aria-hidden=\"true\" size={12} className=\"mr-1 inline\" />\n                  {acceptLabel}\n                </button>\n              ) : null}\n            </>\n          )}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n\nfunction Side({\n  line,\n  side,\n  gutter,\n  numbered,\n}: {\n  line?: DiffLine\n  side: \"before\" | \"after\"\n  gutter: number\n  numbered: boolean\n}) {\n  const number = side === \"before\" ? line?.beforeNumber : line?.afterNumber\n\n  return (\n    <>\n      {numbered ? (\n        <td\n          aria-hidden=\"true\"\n          className={cn(\n            \"text-muted-foreground/60 w-px pl-3 text-right align-top tabular-nums select-none\",\n            line && tone[line.kind]\n          )}\n          style={{ minWidth: `${gutter}ch` }}\n        >\n          {number}\n        </td>\n      ) : null}\n      <td\n        className={cn(\n          \"w-1/2 px-3 align-top\",\n          line ? tone[line.kind] : \"bg-muted/30\"\n        )}\n      >\n        {line ? (\n          <span className=\"break-words whitespace-pre-wrap\">\n            {line.text === \"\" ? \" \" : line.text}\n          </span>\n        ) : null}\n      </td>\n    </>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "code",
    "diff",
    "agent"
  ],
  "type": "registry:ui"
}