{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-unified-viewer",
  "title": "Diff Unified Viewer",
  "description": "A line-addressable unified-diff viewer, split or unified, that swaps red/green blocks for a thin left-rail marker and a muted gutter glyph (colorblind-safe by construction), plus a widget slot for attaching an AI annotation or review comment to any line address.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/diff-unified-viewer/component.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// SeamDiff — a line-addressable unified-diff viewer. Instead of red/green\n// blocks it marks added/removed lines with a thin left-rail glyph (solid\n// bar for additions, a diagonal hairline hatch for deletions, both derived\n// from --foreground) plus a muted +/− gutter glyph, so the diff reads as\n// typography rather than a traffic light and stays legible to colorblind\n// readers. Every rendered line has a stable address (\"n<newLineNo>\" or\n// \"o<oldLineNo>\" for a pure deletion) that a `widgets` map can attach\n// arbitrary React to — an AI annotation, a review comment, an inline\n// suggestion — rendered as a full-width row directly under that line in\n// both unified and split mode. Pure DOM/CSS, no canvas, zero dependencies.\n// A malformed diff never throws: unparsable hunk headers are flagged\n// in place and non-diff input falls back to a plain line list with a\n// visible notice, rather than crashing or silently misreading.\n// ---------------------------------------------------------------------------\n\ntype LineType = \"add\" | \"del\" | \"context\" | \"raw\";\n\ninterface LineEntry {\n  kind: \"line\";\n  type: LineType;\n  content: string;\n  oldNo?: number;\n  newNo?: number;\n}\n\ninterface FileHeaderEntry {\n  kind: \"fileheader\";\n  oldPath?: string;\n  newPath?: string;\n}\n\ninterface HunkEntry {\n  kind: \"hunk\";\n  header: string;\n  oldStart: number;\n  oldLines: number;\n  newStart: number;\n  newLines: number;\n  malformed: boolean;\n  raw: string;\n}\n\ninterface MetaEntry {\n  kind: \"meta\";\n  text: string;\n}\n\ntype Entry = LineEntry | FileHeaderEntry | HunkEntry | MetaEntry;\n\nconst HUNK_RE = /^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@(.*)$/;\n\nfunction cleanPath(raw: string): string {\n  const stripped = (raw.split(\"\\t\")[0] ?? \"\").trim();\n  if (stripped === \"/dev/null\" || stripped === \"\") return stripped;\n  return stripped.replace(/^[ab]\\//, \"\");\n}\n\n/** Parses a unified diff string. Never throws — a line it can't place in a\n * hunk becomes a visible \"raw\" line, and input with no recognizable hunks\n * at all degrades to a flat raw block with `degraded: true`. */\nfunction parseDiff(diff: string): { entries: Entry[]; degraded: boolean } {\n  const lines = diff.replace(/\\r\\n/g, \"\\n\").split(\"\\n\");\n  if (lines.length && lines[lines.length - 1] === \"\") lines.pop();\n\n  const entries: Entry[] = [];\n  let state: \"header\" | \"hunk\" = \"header\";\n  let oldNo = NaN;\n  let newNo = NaN;\n  let sawHunk = false;\n\n  for (const raw of lines) {\n    if (raw.startsWith(\"diff --git\") || raw.startsWith(\"diff \")) {\n      state = \"header\";\n      entries.push({ kind: \"meta\", text: raw });\n      continue;\n    }\n\n    if (\n      state === \"header\" &&\n      /^(index |similarity index|rename from|rename to|new file mode|deleted file mode|old mode|new mode|Binary files)/.test(\n        raw\n      )\n    ) {\n      entries.push({ kind: \"meta\", text: raw });\n      continue;\n    }\n\n    if (state === \"header\" && raw.startsWith(\"--- \")) {\n      entries.push({ kind: \"fileheader\", oldPath: cleanPath(raw.slice(4)) });\n      continue;\n    }\n\n    if (state === \"header\" && raw.startsWith(\"+++ \")) {\n      const prev = entries[entries.length - 1];\n      if (prev && prev.kind === \"fileheader\" && prev.newPath === undefined) {\n        prev.newPath = cleanPath(raw.slice(4));\n      } else {\n        entries.push({ kind: \"fileheader\", newPath: cleanPath(raw.slice(4)) });\n      }\n      continue;\n    }\n\n    if (raw.startsWith(\"@@\")) {\n      sawHunk = true;\n      const m = raw.match(HUNK_RE);\n      if (m) {\n        oldNo = parseInt(m[1] ?? \"0\", 10);\n        newNo = parseInt(m[3] ?? \"0\", 10);\n        entries.push({\n          kind: \"hunk\",\n          header: (m[5] ?? \"\").trim(),\n          oldStart: oldNo,\n          oldLines: m[2] !== undefined ? parseInt(m[2], 10) : 1,\n          newStart: newNo,\n          newLines: m[4] !== undefined ? parseInt(m[4], 10) : 1,\n          malformed: false,\n          raw,\n        });\n      } else {\n        oldNo = NaN;\n        newNo = NaN;\n        entries.push({\n          kind: \"hunk\",\n          header: \"\",\n          oldStart: NaN,\n          oldLines: 0,\n          newStart: NaN,\n          newLines: 0,\n          malformed: true,\n          raw,\n        });\n      }\n      state = \"hunk\";\n      continue;\n    }\n\n    if (state === \"hunk\") {\n      if (raw.startsWith(\"\\\\\")) {\n        entries.push({ kind: \"meta\", text: raw.replace(/^\\\\\\s*/, \"\") });\n        continue;\n      }\n      const marker = raw.charAt(0);\n      const content = raw.slice(1);\n      if (marker === \"+\") {\n        entries.push({\n          kind: \"line\",\n          type: \"add\",\n          content,\n          newNo: Number.isNaN(newNo) ? undefined : newNo++,\n        });\n      } else if (marker === \"-\") {\n        entries.push({\n          kind: \"line\",\n          type: \"del\",\n          content,\n          oldNo: Number.isNaN(oldNo) ? undefined : oldNo++,\n        });\n      } else if (marker === \" \" || raw === \"\") {\n        entries.push({\n          kind: \"line\",\n          type: \"context\",\n          content: marker === \" \" ? content : \"\",\n          oldNo: Number.isNaN(oldNo) ? undefined : oldNo++,\n          newNo: Number.isNaN(newNo) ? undefined : newNo++,\n        });\n      } else {\n        entries.push({ kind: \"line\", type: \"raw\", content: raw });\n      }\n      continue;\n    }\n\n    if (raw.trim() !== \"\") entries.push({ kind: \"meta\", text: raw });\n  }\n\n  if (!sawHunk) {\n    if (diff.trim() === \"\") return { entries: [], degraded: false };\n    return {\n      entries: lines.map((l) => ({ kind: \"line\", type: \"raw\", content: l })),\n      degraded: true,\n    };\n  }\n\n  return { entries, degraded: false };\n}\n\nfunction lineKey(entry: LineEntry): string | undefined {\n  if (entry.newNo != null) return `n${entry.newNo}`;\n  if (entry.oldNo != null) return `o${entry.oldNo}`;\n  return undefined;\n}\n\nfunction widgetFor(\n  entry: LineEntry | undefined,\n  widgets: Record<string, React.ReactNode> | undefined\n): React.ReactNode {\n  if (!entry || !widgets) return undefined;\n  const key = lineKey(entry);\n  return key ? widgets[key] : undefined;\n}\n\n// -- split-mode row pairing: consecutive deletions and consecutive\n// additions are zipped into aligned rows, the shorter side padded with an\n// empty cell; context/raw lines mirror onto both sides. -------------------\n\ntype SplitPairRow = { old?: LineEntry; new?: LineEntry };\n\nfunction buildSplitPairs(entries: Entry[]): (SplitPairRow | { full: FileHeaderEntry | HunkEntry | MetaEntry })[] {\n  const out: (SplitPairRow | { full: FileHeaderEntry | HunkEntry | MetaEntry })[] = [];\n  let delBuf: LineEntry[] = [];\n  let addBuf: LineEntry[] = [];\n  const flush = () => {\n    const n = Math.max(delBuf.length, addBuf.length);\n    for (let i = 0; i < n; i++) out.push({ old: delBuf[i], new: addBuf[i] });\n    delBuf = [];\n    addBuf = [];\n  };\n  for (const e of entries) {\n    if (e.kind === \"line\" && e.type === \"del\") {\n      delBuf.push(e);\n      continue;\n    }\n    if (e.kind === \"line\" && e.type === \"add\") {\n      addBuf.push(e);\n      continue;\n    }\n    flush();\n    if (e.kind === \"line\") {\n      out.push({ old: e, new: e.type === \"context\" ? e : undefined });\n    } else {\n      out.push({ full: e });\n    }\n  }\n  flush();\n  return out;\n}\n\nconst HATCH_STYLE: React.CSSProperties = {\n  backgroundImage:\n    \"repeating-linear-gradient(135deg, var(--foreground) 0px, var(--foreground) 1px, transparent 1px, transparent 4px)\",\n  opacity: 0.4,\n};\n\nfunction railStyle(type: LineType | undefined): { className: string; style?: React.CSSProperties } {\n  if (type === \"add\") return { className: \"bg-foreground/60\" };\n  if (type === \"del\") return { className: \"\", style: HATCH_STYLE };\n  return { className: \"\" };\n}\n\nfunction glyphFor(type: LineType | undefined): string {\n  if (type === \"add\") return \"+\";\n  if (type === \"del\") return \"−\";\n  if (type === \"raw\") return \"?\";\n  return \"\";\n}\n\nfunction washClass(type: LineType | undefined): string {\n  return type === \"add\" || type === \"del\" ? \"bg-foreground/[0.025]\" : \"\";\n}\n\nfunction FileIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.3\"\n      className={className}\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M4 1.5h5L12.5 5v9a1 1 0 0 1-1 1h-7.5a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1Z\"\n        strokeLinejoin=\"round\"\n      />\n      <path d=\"M9 1.5V5h3.5\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction WarnIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.3\"\n      className={className}\n      aria-hidden=\"true\"\n    >\n      <path d=\"M8 1.6 14.6 13a1 1 0 0 1-.87 1.5H2.27a1 1 0 0 1-.87-1.5L8 1.6Z\" strokeLinejoin=\"round\" />\n      <path d=\"M8 6.2v3.1\" strokeLinecap=\"round\" />\n      <circle cx=\"8\" cy=\"11.4\" r=\"0.75\" fill=\"currentColor\" stroke=\"none\" />\n    </svg>\n  );\n}\n\nfunction FullRow({ entry }: { entry: FileHeaderEntry | HunkEntry | MetaEntry }) {\n  if (entry.kind === \"fileheader\") {\n    const label =\n      entry.oldPath && entry.newPath && entry.oldPath !== entry.newPath && entry.oldPath !== \"/dev/null\"\n        ? `${entry.oldPath} → ${entry.newPath}`\n        : entry.newPath && entry.newPath !== \"/dev/null\"\n          ? entry.newPath\n          : (entry.oldPath ?? \"unknown file\");\n    return (\n      <div className=\"flex items-center gap-2 border-y border-border bg-surface px-3 py-1.5 font-mono text-[12px] text-ns-muted\">\n        <FileIcon className=\"h-3 w-3 shrink-0\" />\n        <span className=\"truncate\">{label}</span>\n      </div>\n    );\n  }\n  if (entry.kind === \"hunk\") {\n    if (entry.malformed) {\n      return (\n        <div className=\"flex items-center gap-2 border-y border-dashed border-border px-3 py-1.5 text-[12px] text-ns-muted\">\n          <WarnIcon className=\"h-3 w-3 shrink-0\" />\n          <span>\n            Unparsable hunk header, showing following lines as-is:{\" \"}\n            <span className=\"font-mono\">{entry.raw}</span>\n          </span>\n        </div>\n      );\n    }\n    return (\n      <div className=\"border-y border-border bg-surface px-3 py-1 font-mono text-[12px] tabular-nums text-ns-muted\">\n        {`@@ -${entry.oldStart},${entry.oldLines} +${entry.newStart},${entry.newLines} @@`}\n        {entry.header ? ` ${entry.header}` : \"\"}\n      </div>\n    );\n  }\n  return <div className=\"px-3 py-1 font-mono text-[11px] italic text-ns-muted\">{entry.text}</div>;\n}\n\n// Split mode cannot scroll a long line the way unified can: Tailwind's\n// `grid-cols-2` is `minmax(0,1fr)`, so a pane never grows past half the width\n// and `whitespace-pre` content painted straight over the divider and on top of\n// the other pane's line numbers. Wrapping inside the pane keeps every character\n// readable and the divider centred — and because both halves of a row are\n// `items-stretch` siblings of the same grid, alignment stays exact even when\n// only one side wraps.\nfunction Content({ text, wrap = false }: { text: string; wrap?: boolean }) {\n  return <span className={`${wrap ? \"whitespace-pre-wrap break-all\" : \"whitespace-pre\"} pr-4`}>{text.length ? text : \" \"}</span>;\n}\n\n// -- unified mode -----------------------------------------------------------\n\nfunction UnifiedLine({ entry }: { entry: LineEntry }) {\n  const rail = railStyle(entry.type);\n  const wash = washClass(entry.type);\n  return (\n    <div className=\"grid grid-cols-[3px_4ch_4ch_1.5ch_1fr] items-stretch leading-5 transition-colors hover:bg-foreground/[0.05]\">\n      <span aria-hidden className={rail.className} style={rail.style} />\n      <span className={`select-none py-[3px] pr-2 text-right font-mono text-[12px] tabular-nums text-ns-muted ${wash}`}>\n        {entry.oldNo ?? \"\"}\n      </span>\n      <span className={`select-none py-[3px] pr-2 text-right font-mono text-[12px] tabular-nums text-ns-muted ${wash}`}>\n        {entry.newNo ?? \"\"}\n      </span>\n      <span\n        aria-hidden\n        className={`select-none py-[3px] text-center font-mono text-[12px] ${wash} ${\n          entry.type === \"add\" || entry.type === \"del\" ? \"text-foreground\" : \"text-ns-muted\"\n        }`}\n      >\n        {glyphFor(entry.type)}\n      </span>\n      <span className={`py-[3px] font-mono text-[13px] ${wash}`}>\n        <Content text={entry.content} />\n      </span>\n    </div>\n  );\n}\n\nfunction renderUnified(\n  entries: Entry[],\n  widgets: Record<string, React.ReactNode> | undefined\n): React.ReactNode[] {\n  const out: React.ReactNode[] = [];\n  let key = 0;\n  for (const e of entries) {\n    if (e.kind !== \"line\") {\n      out.push(<FullRow key={key++} entry={e} />);\n      continue;\n    }\n    out.push(<UnifiedLine key={key++} entry={e} />);\n    const widget = widgetFor(e, widgets);\n    if (widget) {\n      out.push(\n        <div key={key++} className=\"border-t border-border bg-surface py-2.5 pl-[calc(3px_+_4ch_+_4ch_+_1.5ch_+_1rem)] pr-4\">\n          {widget}\n        </div>\n      );\n    }\n  }\n  return out;\n}\n\n// -- split mode ---------------------------------------------------------\n\nfunction SplitHalf({ entry, side }: { entry: LineEntry | undefined; side: \"old\" | \"new\" }) {\n  if (!entry) {\n    return (\n      <div className=\"grid grid-cols-[3px_4ch_1.5ch_1fr] items-stretch bg-foreground/[0.015] leading-5 transition-colors group-hover:bg-foreground/[0.05]\">\n        <span aria-hidden />\n        <span />\n        <span />\n        <span />\n      </div>\n    );\n  }\n  const rail = railStyle(entry.type);\n  const wash = washClass(entry.type);\n  const num = side === \"old\" ? entry.oldNo : entry.newNo;\n  return (\n    <div className=\"grid grid-cols-[3px_4ch_1.5ch_1fr] items-stretch leading-5 transition-colors group-hover:bg-foreground/[0.05]\">\n      <span aria-hidden className={rail.className} style={rail.style} />\n      <span className={`select-none py-[3px] pr-2 text-right font-mono text-[12px] tabular-nums text-ns-muted ${wash}`}>\n        {num ?? \"\"}\n      </span>\n      <span\n        aria-hidden\n        className={`select-none py-[3px] text-center font-mono text-[12px] ${wash} ${\n          entry.type === \"add\" || entry.type === \"del\" ? \"text-foreground\" : \"text-ns-muted\"\n        }`}\n      >\n        {glyphFor(entry.type)}\n      </span>\n      {/* the right padding lives on the CELL, not on the inline <span>: inline\n          padding only applies to the last line fragment, so a wrapped line ran\n          flush into the pane's edge. */}\n      <span className={`py-[3px] pr-3 font-mono text-[13px] ${wash}`}>\n        <Content text={entry.content} wrap />\n      </span>\n    </div>\n  );\n}\n\nfunction renderSplit(\n  entries: Entry[],\n  widgets: Record<string, React.ReactNode> | undefined\n): React.ReactNode[] {\n  const items = buildSplitPairs(entries);\n  const out: React.ReactNode[] = [];\n  let key = 0;\n  for (const item of items) {\n    if (\"full\" in item) {\n      out.push(<FullRow key={key++} entry={item.full} />);\n      continue;\n    }\n    const oldWidget = widgetFor(item.old, widgets);\n    const newWidget = item.new !== item.old ? widgetFor(item.new, widgets) : undefined;\n    out.push(\n      <div key={key++} className=\"group grid grid-cols-2\">\n        <SplitHalf entry={item.old} side=\"old\" />\n        <div className=\"border-l border-border\">\n          <SplitHalf entry={item.new} side=\"new\" />\n        </div>\n      </div>\n    );\n    if (oldWidget || newWidget) {\n      out.push(\n        <div key={key++} className=\"grid grid-cols-2 border-t border-border bg-surface\">\n          <div className=\"py-2.5 pl-[calc(3px_+_4ch_+_1.5ch_+_1rem)] pr-4\">{oldWidget}</div>\n          <div className=\"border-l border-border py-2.5 pl-[calc(3px_+_4ch_+_1.5ch_+_1rem)] pr-4\">{newWidget}</div>\n        </div>\n      );\n    }\n  }\n  return out;\n}\n\n// -- mode toggle -------------------------------------------------------------\n\nconst MODES = [\n  { value: \"unified\", label: \"Unified\" },\n  { value: \"split\", label: \"Split\" },\n] as const;\n\ntype Mode = (typeof MODES)[number][\"value\"];\n\nfunction ModeToggle({ mode, onChange }: { mode: Mode; onChange: (m: Mode) => void }) {\n  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    const idx = MODES.findIndex((o) => o.value === mode);\n    let next = -1;\n    if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") next = (idx - 1 + MODES.length) % MODES.length;\n    else if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") next = (idx + 1) % MODES.length;\n    else if (e.key === \"Home\") next = 0;\n    else if (e.key === \"End\") next = MODES.length - 1;\n    else return;\n    e.preventDefault();\n    const target = MODES[next];\n    if (target) onChange(target.value);\n  };\n\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label=\"Diff view\"\n      onKeyDown={onKeyDown}\n      className=\"inline-flex shrink-0 items-center gap-0.5 rounded-sm border border-border bg-foreground/[0.04] p-0.5\"\n    >\n      {MODES.map((opt) => {\n        const selected = opt.value === mode;\n        return (\n          <button\n            key={opt.value}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={selected}\n            tabIndex={selected ? 0 : -1}\n            onClick={() => onChange(opt.value)}\n            className={`rounded-[4px] px-2.5 py-1 font-mono text-[11px] uppercase tracking-wide outline-none transition-colors motion-reduce:transition-none focus-visible:ring-2 focus-visible:ring-ns-accent ${\n              // The *selected* segment needs a hover affordance too, not just the\n              // unselected one: hovering the currently-active view was previously\n              // a dead pixel-for-pixel no-op, which reads as an inert label\n              // rather than a control you can point at. `ring`, not `outline` —\n              // the base class sets `outline-none`, which pins `--tw-outline-style`\n              // to none and silently swallows any hover outline width. Tailwind\n              // orders `focus-visible:` after `hover:`, so the accent focus ring\n              // still wins over this when both apply.\n              selected\n                ? \"bg-background text-foreground shadow-sm hover:ring-1 hover:ring-inset hover:ring-border\"\n                : \"text-ns-muted hover:text-foreground\"\n            }`}\n          >\n            {opt.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\n// -- public component ---------------------------------------------------\n\nexport interface SeamDiffProps {\n  /** A unified-diff string (as produced by `git diff`, `diff -u`, etc). */\n  diff: string;\n  /** Controlled view mode. Omit to let the component manage its own. */\n  mode?: Mode;\n  /** Initial view mode when uncontrolled. @default \"unified\" */\n  defaultMode?: Mode;\n  /** Fires whenever the view mode changes, from a click or the keyboard. */\n  onModeChange?: (mode: Mode) => void;\n  /**\n   * Arbitrary React keyed by line address, rendered as a full-width row\n   * directly under that line. A line's address is `n<newLineNo>` for any\n   * line that exists in the new file (context or addition), or\n   * `o<oldLineNo>` for a line that only exists in the old file (a pure\n   * deletion) — e.g. `{ \"n42\": <AiSuggestion />, \"o17\": <ReviewNote /> }`.\n   */\n  widgets?: Record<string, React.ReactNode>;\n  /** Accessible name for the scrollable diff region. */\n  ariaLabel?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function SeamDiff({\n  diff,\n  mode,\n  defaultMode = \"unified\",\n  onModeChange,\n  widgets,\n  ariaLabel,\n  className = \"\",\n}: SeamDiffProps) {\n  const isControlled = mode !== undefined;\n  const [internalMode, setInternalMode] = useState<Mode>(defaultMode);\n  const activeMode = isControlled ? mode : internalMode;\n\n  const setMode = (m: Mode) => {\n    if (!isControlled) setInternalMode(m);\n    if (m !== activeMode) onModeChange?.(m);\n  };\n\n  const parsed = useMemo(() => parseDiff(diff ?? \"\"), [diff]);\n\n  const { filePath, renamedFrom, added, removed } = useMemo(() => {\n    let fh: FileHeaderEntry | undefined;\n    let a = 0;\n    let r = 0;\n    for (const e of parsed.entries) {\n      if (e.kind === \"fileheader\" && !fh) fh = e;\n      else if (e.kind === \"line\") {\n        if (e.type === \"add\") a++;\n        else if (e.type === \"del\") r++;\n      }\n    }\n    const newPath = fh?.newPath && fh.newPath !== \"/dev/null\" ? fh.newPath : undefined;\n    const oldPath = fh?.oldPath && fh.oldPath !== \"/dev/null\" ? fh.oldPath : undefined;\n    const renamed = !!(newPath && oldPath && newPath !== oldPath);\n    return {\n      filePath: newPath ?? oldPath,\n      renamedFrom: renamed ? oldPath : undefined,\n      added: a,\n      removed: r,\n    };\n  }, [parsed]);\n\n  const rows = useMemo(() => {\n    if (parsed.degraded) return null;\n    return activeMode === \"split\" ? renderSplit(parsed.entries, widgets) : renderUnified(parsed.entries, widgets);\n  }, [parsed, activeMode, widgets]);\n\n  const regionLabel = ariaLabel ?? (filePath ? `Diff for ${filePath}` : \"Diff\");\n\n  return (\n    <div className={`flex flex-col overflow-hidden rounded-md border border-border bg-background text-foreground ${className}`}>\n      <div className=\"flex flex-wrap items-center justify-between gap-3 border-b border-border bg-surface px-3 py-2\">\n        <div className=\"flex min-w-0 items-center gap-2 font-mono text-[13px]\">\n          <FileIcon className=\"h-3.5 w-3.5 shrink-0 text-ns-muted\" />\n          <span className=\"truncate\">\n            {renamedFrom ? (\n              <>\n                <span className=\"text-ns-muted\">{renamedFrom}</span>\n                <span className=\"mx-1 text-ns-muted\">{\"→\"}</span>\n                <span>{filePath}</span>\n              </>\n            ) : (\n              <span>{filePath ?? \"diff\"}</span>\n            )}\n          </span>\n          {(added > 0 || removed > 0) && (\n            <span className=\"ml-1 shrink-0 font-mono text-[12px] tabular-nums text-ns-muted\">\n              +{added} {\"−\"}\n              {removed}\n            </span>\n          )}\n        </div>\n        <ModeToggle mode={activeMode} onChange={setMode} />\n        <span aria-live=\"polite\" className=\"sr-only\">\n          {activeMode === \"split\" ? \"Split view\" : \"Unified view\"}\n        </span>\n      </div>\n\n      {parsed.degraded ? (\n        <div>\n          <div className=\"flex items-center gap-2 border-b border-dashed border-border px-3 py-1.5 text-[12px] text-ns-muted\">\n            <WarnIcon className=\"h-3 w-3 shrink-0\" />\n            <span>This doesn&apos;t look like a unified diff, showing the raw text below.</span>\n          </div>\n          <div role=\"region\" aria-label={regionLabel} className=\"overflow-x-auto\">\n            {parsed.entries.map((e, i) =>\n              e.kind === \"line\" ? (\n                <div key={i} className=\"whitespace-pre px-3 py-[3px] font-mono text-[13px] leading-5\">\n                  {e.content.length ? e.content : \" \"}\n                </div>\n              ) : null\n            )}\n          </div>\n        </div>\n      ) : parsed.entries.length === 0 ? (\n        <div className=\"px-3 py-6 text-center font-mono text-[13px] text-ns-muted\">Nothing to show.</div>\n      ) : (\n        <div role=\"region\" aria-label={regionLabel} className=\"overflow-x-auto\">\n          {rows}\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/diff-unified-viewer.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "diff",
      "code-review",
      "developer-tools",
      "typography",
      "accessibility",
      "split-view",
      "unified-diff"
    ],
    "instruction": "Renders a unified-diff string (as produced by `git diff` / `diff -u`) as either a single-column unified view or a two-pane split view, toggled by an accessible role=radiogroup labeled 'Diff view' with role=radio Unified/Split buttons (arrow keys move focus and selection, aria-checked reflects state, the change is also announced through a visually-hidden aria-live=polite region). PARSING: a hand-written state machine reads file headers (`--- a/path`, `+++ b/path`, with git's `a/`/`b/` prefixes and any trailing tab-separated timestamp stripped, `/dev/null` recognized for adds/deletes), `diff --git`/`index`/`rename from|to`/`similarity index`/`old mode`/`new mode`/`Binary files` as meta lines, hunk headers matched by `/^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@(.*)$/` seeding running old/new line counters, `+`/`-`/` ` markers for add/del/context, and `\\ No newline at end of file`. It never throws: an unparsable `@@` line is flagged in place with a warning icon and the following lines still render (numberless) rather than aborting; input with no recognizable hunk at all falls back to a flat, numberless raw-line block behind a visible 'doesn't look like a unified diff' notice, never a crash or a silently wrong parse. COLOR: every added/removed line gets a 3px left-rail marker plus a muted +/− gutter glyph instead of red/green fill — additions get a solid bar (bg-foreground/60), deletions get a 135° diagonal hairline hatch (repeating-linear-gradient read off var(--foreground)) — so the add/del distinction survives grayscale and every common colorblindness type; a very light bg-foreground/[0.025] wash marks changed rows without relying on hue at all. Every diff line (in both modes; in split mode the whole two-pane row via a group-hover) lifts to a slightly stronger bg-foreground/[0.05] on hover so the line the pointer is over reads as targetable for the annotation/widget slot — a tokened wash, never a hue. Line numbers (both old and new, in unified mode; the relevant one only, in split mode) are font-mono tabular-nums. SPLIT MODE: consecutive deletions and consecutive additions within a hunk are zipped into aligned rows (shorter side padded with an empty cell), context lines mirror onto both sides — the standard side-by-side diff algorithm, not a naive two-array print. WIDGET SLOT: an optional `widgets` prop, `Record<string, ReactNode>`, keyed by a stable per-line address — `n<newLineNo>` for any line that exists in the new file (context or addition), `o<oldLineNo>` for a line that only exists in the old file (a pure deletion). A matched widget renders as a full-width row directly beneath that line, in both view modes, so an agent's review note, a teammate's comment, or a suggested-edit card with its own controls (e.g. an 'Apply suggestion' button) sits inline at the exact line it concerns rather than in a disconnected side panel. Widget insertion points also serve as hard segment breaks for the two-pane grid in split mode, which is what keeps the left/right row counts — and therefore the visual alignment — exact even when only one side of a pair carries a widget. PERFORMANCE: parsing and row construction are pure, memoized on `diff`/mode/`widgets`, and rendering is a flat list of plain DOM rows (CSS Grid per row for column alignment) with no virtualization needed for a several-hundred-line diff. REDUCED MOTION: the only animation is the mode toggle's color transition, dropped entirely via motion-reduce:transition-none; nothing else moves, so there's nothing to lose. Every color is a token (--background/--foreground/--ns-muted/--border/--ns-accent, plus --surface for header/hunk/widget bands); --ns-accent appears only on the toggle's focus ring, never as decoration. Zero dependencies, no canvas."
  },
  "type": "registry:ui"
}