{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-block",
  "type": "registry:ui",
  "title": "DiffBlock",
  "description": "Git patch viewer with line gutters, additions, and deletions.",
  "registryDependencies": [
    "https://ui.arpitbhayani.me/r/aui.json"
  ],
  "files": [
    {
      "path": "ui/diff-block.tsx",
      "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type DiffCoverage = \"covered\" | \"uncovered\" | \"partial\";\nexport type DiffView = \"unified\" | \"split\";\n\nexport interface DiffSegment {\n  text: string;\n  changed?: boolean;\n}\n\nexport interface DiffLine {\n  type: \"add\" | \"del\" | \"normal\" | \"meta\";\n  content: string;\n  oldNum?: number | string;\n  newNum?: number | string;\n  /** Test coverage of this line in the new file. Rendered when `showCoverage` is set. */\n  coverage?: DiffCoverage;\n  /** Pre-computed intra-line segments. Overrides automatic word highlighting. */\n  segments?: DiffSegment[];\n  /** Author or reviewer annotation rendered beneath the line. */\n  note?: string;\n}\n\nexport type DiffRow =\n  | { kind: \"line\"; line: DiffLine }\n  | { kind: \"pair\"; left?: DiffLine; right?: DiffLine }\n  | { kind: \"fold\"; rows: DiffRow[]; count: number };\n\nexport type DiffFileStatus = \"added\" | \"deleted\" | \"renamed\" | \"modified\";\n\nexport interface DiffBlockProps extends React.HTMLAttributes<HTMLDivElement> {\n  file?: string;\n  diff?: string;\n  lines?: DiffLine[];\n  /** File status badge shown in the header ('added' | 'deleted' | 'renamed' | 'modified'). */\n  status?: DiffFileStatus;\n  fileStatus?: DiffFileStatus;\n  /** `unified` (default) or side-by-side `split`. */\n  view?: DiffView;\n  /** Keep N unchanged lines around each change and fold the rest. `false` shows everything. */\n  collapseContext?: number | false;\n  /** Paint a coverage rail from each line's `coverage` value. */\n  showCoverage?: boolean;\n  /** Highlight changed words inside paired deleted/added lines. Default true. */\n  highlightWords?: boolean;\n}\n\nconst HUNK_RE = /^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@/;\nconst FILE_HEADER_RE = /^(diff --git |index |--- |\\+\\+\\+ |new file mode|deleted file mode|similarity index|rename (from|to) )/;\n\nexport function parseDiff(raw: string): { lines: DiffLine[]; adds: number; dels: number } {\n  const rawLines = raw.replace(/\\s+$/, \"\").replace(/^\\n+/, \"\").split(\"\\n\");\n  const hasHunks = rawLines.some((l) => l.startsWith(\"@@\"));\n  const lines: DiffLine[] = [];\n  let adds = 0;\n  let dels = 0;\n  let oldIndex = 1;\n  let newIndex = 1;\n  let inHunk = false;\n\n  for (const line of rawLines) {\n    if (line.startsWith(\"@@\")) {\n      const m = HUNK_RE.exec(line);\n      if (m) {\n        oldIndex = parseInt(m[1], 10);\n        newIndex = parseInt(m[2], 10);\n      }\n      inHunk = true;\n      lines.push({ type: \"meta\", content: line, oldNum: \"...\", newNum: \"...\" });\n    } else if (hasHunks && !inHunk && FILE_HEADER_RE.test(line)) {\n      continue;\n    } else if (line.startsWith(\"\\\\\")) {\n      lines.push({ type: \"meta\", content: line });\n    } else if (line.startsWith(\"+\")) {\n      adds++;\n      lines.push({ type: \"add\", content: line.slice(1), newNum: newIndex++ });\n    } else if (line.startsWith(\"-\")) {\n      dels++;\n      lines.push({ type: \"del\", content: line.slice(1), oldNum: oldIndex++ });\n    } else {\n      lines.push({\n        type: \"normal\",\n        content: line.startsWith(\" \") ? line.slice(1) : line,\n        oldNum: oldIndex++,\n        newNum: newIndex++,\n      });\n    }\n  }\n\n  return { lines, adds, dels };\n}\n\nfunction tokenize(s: string): string[] {\n  return s.match(/\\s+|[A-Za-z0-9_]+|[^\\sA-Za-z0-9_]/g) ?? [];\n}\n\nfunction wordSegments(oldText: string, newText: string): [DiffSegment[], DiffSegment[]] | null {\n  const a = tokenize(oldText);\n  const b = tokenize(newText);\n  let p = 0;\n  while (p < a.length && p < b.length && a[p] === b[p]) p++;\n  let s = 0;\n  while (s < a.length - p && s < b.length - p && a[a.length - 1 - s] === b[b.length - 1 - s]) s++;\n  if (p === 0 && s === 0) return null;\n  if (p + s >= a.length && p + s >= b.length) return null;\n\n  const build = (t: string[]): DiffSegment[] => {\n    const segs: DiffSegment[] = [];\n    const pre = t.slice(0, p).join(\"\");\n    const mid = t.slice(p, t.length - s).join(\"\");\n    const suf = t.slice(t.length - s).join(\"\");\n    if (pre) segs.push({ text: pre });\n    if (mid) segs.push({ text: mid, changed: true });\n    if (suf) segs.push({ text: suf });\n    return segs;\n  };\n  return [build(a), build(b)];\n}\n\n/** Splits lines into [delRun, addRun] groups (either may be empty) and passthrough singles. */\nfunction groupRuns(lines: DiffLine[]): { dels: DiffLine[]; adds: DiffLine[]; single?: DiffLine }[] {\n  const groups: { dels: DiffLine[]; adds: DiffLine[]; single?: DiffLine }[] = [];\n  let i = 0;\n  while (i < lines.length) {\n    const l = lines[i];\n    if (l.type === \"del\" || l.type === \"add\") {\n      const dels: DiffLine[] = [];\n      const adds: DiffLine[] = [];\n      while (i < lines.length && lines[i].type === \"del\") dels.push(lines[i++]);\n      while (i < lines.length && lines[i].type === \"add\") adds.push(lines[i++]);\n      groups.push({ dels, adds });\n    } else {\n      groups.push({ dels: [], adds: [], single: l });\n      i++;\n    }\n  }\n  return groups;\n}\n\nfunction annotateWords(lines: DiffLine[]): DiffLine[] {\n  const out = lines.map((l) => ({ ...l }));\n  const byRef = new Map<DiffLine, DiffLine>(lines.map((l, i) => [l, out[i]]));\n  for (const g of groupRuns(lines)) {\n    const n = Math.min(g.dels.length, g.adds.length);\n    for (let k = 0; k < n; k++) {\n      const d = byRef.get(g.dels[k])!;\n      const a = byRef.get(g.adds[k])!;\n      if (d.segments || a.segments) continue;\n      const segs = wordSegments(d.content, a.content);\n      if (segs) {\n        d.segments = segs[0];\n        a.segments = segs[1];\n      }\n    }\n  }\n  return out;\n}\n\nfunction rowIsChanged(row: DiffRow): boolean {\n  if (row.kind === \"line\") return row.line.type === \"add\" || row.line.type === \"del\";\n  if (row.kind === \"pair\") return [row.left, row.right].some((l) => l && l.type !== \"normal\");\n  return false;\n}\n\nfunction rowIsPinned(row: DiffRow): boolean {\n  if (row.kind === \"line\") return row.line.type === \"meta\" || Boolean(row.line.note);\n  if (row.kind === \"pair\") return [row.left, row.right].some((l) => l && l.note);\n  return true;\n}\n\nexport function buildDiffRows(\n  inputLines: DiffLine[],\n  opts: { view?: DiffView; collapseContext?: number | false; highlightWords?: boolean } = {}\n): DiffRow[] {\n  const { view = \"unified\", collapseContext = false, highlightWords = true } = opts;\n  const lines = highlightWords ? annotateWords(inputLines) : inputLines;\n\n  let rows: DiffRow[];\n  if (view === \"split\") {\n    rows = [];\n    for (const g of groupRuns(lines)) {\n      if (g.single) {\n        rows.push(\n          g.single.type === \"meta\"\n            ? { kind: \"line\", line: g.single }\n            : { kind: \"pair\", left: g.single, right: g.single }\n        );\n        continue;\n      }\n      const n = Math.max(g.dels.length, g.adds.length);\n      for (let k = 0; k < n; k++) rows.push({ kind: \"pair\", left: g.dels[k], right: g.adds[k] });\n    }\n  } else {\n    rows = lines.map((line) => ({ kind: \"line\", line }));\n  }\n\n  if (collapseContext === false || collapseContext < 0) return rows;\n\n  const keep = rows.map(rowIsPinned);\n  rows.forEach((r, i) => {\n    if (!rowIsChanged(r)) return;\n    for (let k = Math.max(0, i - collapseContext); k <= Math.min(rows.length - 1, i + collapseContext); k++) {\n      keep[k] = true;\n    }\n  });\n\n  const out: DiffRow[] = [];\n  let i = 0;\n  while (i < rows.length) {\n    if (keep[i]) {\n      out.push(rows[i++]);\n      continue;\n    }\n    const start = i;\n    while (i < rows.length && !keep[i]) i++;\n    const hidden = rows.slice(start, i);\n    if (hidden.length >= 2) out.push({ kind: \"fold\", rows: hidden, count: hidden.length });\n    else out.push(...hidden);\n  }\n  return out;\n}\n\nconst SYMBOLS: Record<DiffLine[\"type\"], string> = { add: \"+\", del: \"-\", meta: \"@\", normal: \" \" };\n\nconst LINE_STYLE: Record<DiffLine[\"type\"], string> = {\n  add: \"bg-emerald-950/40 text-emerald-300 border-l-2 border-emerald-500\",\n  del: \"bg-rose-950/40 text-rose-300 border-l-2 border-rose-500\",\n  meta: \"text-cyan-400 bg-cyan-950/20 font-semibold\",\n  normal: \"text-zinc-300 hover:bg-zinc-800/40\",\n};\n\nconst WORD_STYLE: Record<DiffLine[\"type\"], string> = {\n  add: \"bg-emerald-500/30 rounded-xs\",\n  del: \"bg-rose-500/30 rounded-xs\",\n  meta: \"\",\n  normal: \"\",\n};\n\nconst COVERAGE_STYLE: Record<DiffCoverage, string> = {\n  covered: \"bg-emerald-500\",\n  uncovered: \"bg-rose-500\",\n  partial: \"bg-amber-400\",\n};\n\nfunction renderText(l: DiffLine) {\n  if (!l.segments) return l.content;\n  return l.segments.map((s, i) =>\n    s.changed ? (\n      <span key={i} className={WORD_STYLE[l.type]}>\n        {s.text}\n      </span>\n    ) : (\n      <React.Fragment key={i}>{s.text}</React.Fragment>\n    )\n  );\n}\n\nfunction CoverageRail({ coverage, show }: { coverage?: DiffCoverage; show: boolean }) {\n  if (!show) return null;\n  return (\n    <span\n      className={cn(\"self-stretch w-[3px] mr-2 rounded-xs flex-shrink-0\", coverage && COVERAGE_STYLE[coverage])}\n      title={coverage ? `Coverage: ${coverage}` : undefined}\n      aria-label={coverage ? `Coverage: ${coverage}` : undefined}\n    />\n  );\n}\n\nfunction Note({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"pl-[4.25rem] pr-3.5 py-1.5 bg-amber-950/25 border-l-2 border-amber-400 text-xs text-zinc-300 font-sans leading-snug\">\n      {children}\n    </div>\n  );\n}\n\nfunction UnifiedLine({ l, showCoverage }: { l: DiffLine; showCoverage: boolean }) {\n  if (l.type === \"meta\") {\n    return (\n      <>\n        <div className=\"flex items-center px-4 py-1 text-xs italic text-zinc-500 bg-[#121217]/60 border-y border-[#24242c]/50 font-mono select-none\">\n          <span>{renderText(l)}</span>\n        </div>\n        {l.note && <Note>{l.note}</Note>}\n      </>\n    );\n  }\n  return (\n    <>\n      <div className={cn(\"flex items-start px-2 py-0.5 rounded-xs transition-colors whitespace-pre\", LINE_STYLE[l.type])}>\n        <CoverageRail coverage={l.coverage} show={showCoverage} />\n        <span className=\"flex gap-2 pr-3 select-none text-[0.72rem] text-zinc-600 flex-shrink-0\">\n          <span className=\"min-w-[3ch] text-right\">{l.oldNum ?? \"\"}</span>\n          <span className=\"min-w-[3ch] text-right\">{l.newNum ?? \"\"}</span>\n        </span>\n        <span className=\"w-4 select-none text-zinc-500 font-semibold flex-shrink-0\">{SYMBOLS[l.type]}</span>\n        <span className=\"flex-1\">{renderText(l)}</span>\n      </div>\n      {l.note && <Note>{l.note}</Note>}\n    </>\n  );\n}\n\nfunction SplitHalf({ l, side, showCoverage }: { l?: DiffLine; side: \"left\" | \"right\"; showCoverage: boolean }) {\n  if (!l) return <div className=\"bg-zinc-900/60\" />;\n  if (l.type === \"meta\") {\n    return (\n      <div className=\"flex items-center px-4 py-1 text-xs italic text-zinc-500 bg-[#121217]/60 border-y border-[#24242c]/50 font-mono select-none\">\n        <span>{renderText(l)}</span>\n      </div>\n    );\n  }\n  return (\n    <div\n      className={cn(\n        \"flex items-start min-w-0 px-2 py-0.5 whitespace-pre\",\n        side === \"right\" && \"border-l border-[#24242c]\",\n        LINE_STYLE[l.type]\n      )}\n    >\n      {side === \"right\" && <CoverageRail coverage={l.coverage} show={showCoverage} />}\n      <span className=\"pr-3 select-none text-[0.72rem] text-zinc-600 min-w-[3ch] text-right flex-shrink-0\">\n        {(side === \"left\" ? l.oldNum : l.newNum) ?? \"\"}\n      </span>\n      <span className=\"w-4 select-none text-zinc-500 font-semibold flex-shrink-0\">{SYMBOLS[l.type]}</span>\n      <span className=\"flex-1\">{renderText(l)}</span>\n    </div>\n  );\n}\n\nfunction Rows({ rows, showCoverage }: { rows: DiffRow[]; showCoverage: boolean }) {\n  return (\n    <>\n      {rows.map((row, idx) => {\n        if (row.kind === \"line\") return <UnifiedLine key={idx} l={row.line} showCoverage={showCoverage} />;\n        if (row.kind === \"pair\") {\n          const note = row.right?.note ?? row.left?.note;\n          return (\n            <React.Fragment key={idx}>\n              <div className=\"grid grid-cols-[minmax(20rem,1fr)_minmax(20rem,1fr)]\">\n                <SplitHalf l={row.left} side=\"left\" showCoverage={showCoverage} />\n                <SplitHalf l={row.right} side=\"right\" showCoverage={showCoverage} />\n              </div>\n              {note && <Note>{note}</Note>}\n            </React.Fragment>\n          );\n        }\n        return (\n          <details key={idx} className=\"group\">\n            <summary className=\"cursor-pointer list-none px-3.5 py-1 bg-[#121217] border-y border-[#24242c] text-xs italic text-zinc-500 hover:text-zinc-300 select-none [&::-webkit-details-marker]:hidden group-open:not-italic\">\n              {row.count} unchanged lines\n            </summary>\n            <Rows rows={row.rows} showCoverage={showCoverage} />\n          </details>\n        );\n      })}\n    </>\n  );\n}\n\nexport function DiffBlock({\n  file,\n  status,\n  fileStatus,\n  diff,\n  lines: customLines,\n  view = \"unified\",\n  collapseContext = false,\n  showCoverage = false,\n  highlightWords = true,\n  className,\n  ...props\n}: DiffBlockProps) {\n  const parsed = React.useMemo(\n    () => (diff ? parseDiff(diff) : { lines: customLines || [], adds: 0, dels: 0 }),\n    [diff, customLines]\n  );\n  const lines = customLines || parsed.lines;\n  const adds = customLines ? customLines.filter((l) => l.type === \"add\").length : parsed.adds;\n  const dels = customLines ? customLines.filter((l) => l.type === \"del\").length : parsed.dels;\n  const rows = React.useMemo(\n    () => buildDiffRows(lines, { view, collapseContext, highlightWords }),\n    [lines, view, collapseContext, highlightWords]\n  );\n  const effectiveStatus = status || fileStatus;\n\n  return (\n    <div\n      className={cn(\n        \"rounded-lg border border-border bg-[#070709] text-[#ececf1] font-mono text-[0.84rem] overflow-hidden shadow-md my-4\",\n        className\n      )}\n      {...props}\n    >\n      {file && (\n        <div className=\"flex items-center justify-between px-3.5 py-2 bg-[#121217] border-b border-[#24242c] text-xs\">\n          <div className=\"flex items-center gap-2 min-w-0\">\n            <span className=\"text-zinc-300 font-medium truncate\">{file}</span>\n            {effectiveStatus && (\n              <span\n                className={cn(\n                  \"text-[0.68rem] font-mono uppercase px-1.5 py-0.5 rounded-xs font-medium tracking-wider\",\n                  effectiveStatus === \"added\" && \"bg-emerald-500/15 text-emerald-400 border border-emerald-500/30\",\n                  effectiveStatus === \"deleted\" && \"bg-rose-500/15 text-rose-400 border border-rose-500/30\",\n                  effectiveStatus === \"renamed\" && \"bg-amber-500/15 text-amber-400 border border-amber-500/30\",\n                  effectiveStatus === \"modified\" && \"bg-zinc-700/30 text-zinc-400 border border-zinc-700/50\"\n                )}\n              >\n                {effectiveStatus === \"added\" ? \"+ added\" : effectiveStatus === \"deleted\" ? \"− deleted\" : effectiveStatus}\n              </span>\n            )}\n          </div>\n          <div className=\"flex items-center gap-2 font-mono text-[0.75rem] shrink-0\">\n            {adds > 0 && <span className=\"text-emerald-400\">+{adds}</span>}\n            {dels > 0 && <span className=\"text-rose-400\">-{dels}</span>}\n          </div>\n        </div>\n      )}\n      <div className=\"py-2 overflow-x-auto leading-relaxed\">\n        <Rows rows={rows} showCoverage={showCoverage} />\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ]
}