{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-diff-viewer",
  "title": "Code Diff Viewer",
  "description": "Side-by-side or unified diff view with syntax highlighting via Shiki",
  "dependencies": [
    "shiki"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/devtools/code-diff-viewer/components/elements/code-diff-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { codeToHtml } from \"shiki\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype DiffMode = \"unified\" | \"split\";\n\ninterface CodeDiffViewerProps {\n  oldCode: string;\n  newCode: string;\n  language?: string;\n  mode?: DiffMode;\n  showLineNumbers?: boolean;\n  className?: string;\n}\n\ninterface DiffLine {\n  type: \"unchanged\" | \"added\" | \"removed\";\n  content: string;\n  oldLineNum?: number;\n  newLineNum?: number;\n}\n\nfunction computeDiff(oldCode: string, newCode: string): DiffLine[] {\n  const oldLines = oldCode.split(\"\\n\");\n  const newLines = newCode.split(\"\\n\");\n  const result: DiffLine[] = [];\n\n  let oldIdx = 0;\n  let newIdx = 0;\n  let oldLineNum = 1;\n  let newLineNum = 1;\n\n  while (oldIdx < oldLines.length || newIdx < newLines.length) {\n    const oldLine = oldLines[oldIdx];\n    const newLine = newLines[newIdx];\n\n    if (oldIdx >= oldLines.length) {\n      result.push({\n        type: \"added\",\n        content: newLine,\n        newLineNum: newLineNum++,\n      });\n      newIdx++;\n    } else if (newIdx >= newLines.length) {\n      result.push({\n        type: \"removed\",\n        content: oldLine,\n        oldLineNum: oldLineNum++,\n      });\n      oldIdx++;\n    } else if (oldLine === newLine) {\n      result.push({\n        type: \"unchanged\",\n        content: oldLine,\n        oldLineNum: oldLineNum++,\n        newLineNum: newLineNum++,\n      });\n      oldIdx++;\n      newIdx++;\n    } else {\n      const oldRemaining = oldLines.slice(oldIdx);\n      const newRemaining = newLines.slice(newIdx);\n      const newLineInOld = oldRemaining.indexOf(newLine);\n      const oldLineInNew = newRemaining.indexOf(oldLine);\n\n      if (newLineInOld === -1 && oldLineInNew === -1) {\n        result.push({\n          type: \"removed\",\n          content: oldLine,\n          oldLineNum: oldLineNum++,\n        });\n        result.push({\n          type: \"added\",\n          content: newLine,\n          newLineNum: newLineNum++,\n        });\n        oldIdx++;\n        newIdx++;\n      } else if (\n        newLineInOld !== -1 &&\n        (oldLineInNew === -1 || newLineInOld <= oldLineInNew)\n      ) {\n        result.push({\n          type: \"removed\",\n          content: oldLine,\n          oldLineNum: oldLineNum++,\n        });\n        oldIdx++;\n      } else {\n        result.push({\n          type: \"added\",\n          content: newLine,\n          newLineNum: newLineNum++,\n        });\n        newIdx++;\n      }\n    }\n  }\n\n  return result;\n}\n\nfunction UnifiedDiff({\n  diff,\n  language,\n  showLineNumbers,\n}: {\n  diff: DiffLine[];\n  language: string;\n  showLineNumbers: boolean;\n}) {\n  const [highlightedLines, setHighlightedLines] = React.useState<\n    Map<number, string>\n  >(new Map());\n\n  React.useEffect(() => {\n    async function highlightAll() {\n      const results = new Map<number, string>();\n      for (let i = 0; i < diff.length; i++) {\n        const line = diff[i];\n        if (line.content) {\n          const html = await codeToHtml(line.content, {\n            lang: language,\n            themes: { light: \"github-light\", dark: \"github-dark\" },\n            defaultColor: false,\n          });\n          const match = html.match(/<code[^>]*>([\\s\\S]*?)<\\/code>/);\n          results.set(i, match ? match[1] : line.content);\n        } else {\n          results.set(i, \"\");\n        }\n      }\n      setHighlightedLines(results);\n    }\n    highlightAll();\n  }, [diff, language]);\n\n  return (\n    <div className=\"font-mono text-sm overflow-auto\">\n      {diff.map((line, idx) => (\n        <div\n          key={idx}\n          className={cn(\n            \"flex\",\n            line.type === \"added\" && \"bg-green-100 dark:bg-green-950/50\",\n            line.type === \"removed\" && \"bg-red-100 dark:bg-red-950/50\",\n          )}\n        >\n          {showLineNumbers && (\n            <div className=\"flex shrink-0 text-muted-foreground text-xs\">\n              <span className=\"w-10 px-2 text-right border-r border-border\">\n                {line.oldLineNum ?? \"\"}\n              </span>\n              <span className=\"w-10 px-2 text-right border-r border-border\">\n                {line.newLineNum ?? \"\"}\n              </span>\n            </div>\n          )}\n          <span\n            className={cn(\n              \"w-6 shrink-0 text-center\",\n              line.type === \"added\" && \"text-green-600 dark:text-green-400\",\n              line.type === \"removed\" && \"text-red-600 dark:text-red-400\",\n            )}\n          >\n            {line.type === \"added\" ? \"+\" : line.type === \"removed\" ? \"-\" : \" \"}\n          </span>\n          <span\n            className=\"flex-1 px-2\"\n            dangerouslySetInnerHTML={{\n              __html: highlightedLines.get(idx) ?? line.content ?? \"\",\n            }}\n          />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction SplitDiff({\n  diff,\n  language,\n  showLineNumbers,\n}: {\n  diff: DiffLine[];\n  language: string;\n  showLineNumbers: boolean;\n}) {\n  const [highlightedLines, setHighlightedLines] = React.useState<\n    Map<number, string>\n  >(new Map());\n\n  const leftLines: DiffLine[] = [];\n  const rightLines: DiffLine[] = [];\n\n  for (const line of diff) {\n    if (line.type === \"unchanged\") {\n      leftLines.push(line);\n      rightLines.push(line);\n    } else if (line.type === \"removed\") {\n      leftLines.push(line);\n    } else {\n      rightLines.push(line);\n    }\n  }\n\n  const maxLen = Math.max(leftLines.length, rightLines.length);\n  while (leftLines.length < maxLen)\n    leftLines.push({ type: \"unchanged\", content: \"\" });\n  while (rightLines.length < maxLen)\n    rightLines.push({ type: \"unchanged\", content: \"\" });\n\n  React.useEffect(() => {\n    async function highlightAll() {\n      const results = new Map<number, string>();\n      const allLines = [...leftLines, ...rightLines];\n      for (let i = 0; i < allLines.length; i++) {\n        const line = allLines[i];\n        if (line.content) {\n          const html = await codeToHtml(line.content, {\n            lang: language,\n            themes: { light: \"github-light\", dark: \"github-dark\" },\n            defaultColor: false,\n          });\n          const match = html.match(/<code[^>]*>([\\s\\S]*?)<\\/code>/);\n          results.set(i, match ? match[1] : line.content);\n        } else {\n          results.set(i, \"\");\n        }\n      }\n      setHighlightedLines(results);\n    }\n    highlightAll();\n  }, [diff, language]);\n\n  return (\n    <div className=\"font-mono text-sm overflow-auto flex\">\n      <div className=\"flex-1 border-r border-border\">\n        {leftLines.map((line, idx) => (\n          <div\n            key={idx}\n            className={cn(\n              \"flex\",\n              line.type === \"removed\" && \"bg-red-100 dark:bg-red-950/50\",\n            )}\n          >\n            {showLineNumbers && (\n              <span className=\"w-10 px-2 text-right text-muted-foreground text-xs border-r border-border shrink-0\">\n                {line.oldLineNum ?? \"\"}\n              </span>\n            )}\n            <span\n              className={cn(\n                \"w-6 shrink-0 text-center\",\n                line.type === \"removed\" && \"text-red-600 dark:text-red-400\",\n              )}\n            >\n              {line.type === \"removed\" ? \"-\" : \" \"}\n            </span>\n            <span\n              className=\"flex-1 px-2\"\n              dangerouslySetInnerHTML={{\n                __html: highlightedLines.get(idx) ?? line.content ?? \"\",\n              }}\n            />\n          </div>\n        ))}\n      </div>\n      <div className=\"flex-1\">\n        {rightLines.map((line, idx) => (\n          <div\n            key={idx}\n            className={cn(\n              \"flex\",\n              line.type === \"added\" && \"bg-green-100 dark:bg-green-950/50\",\n            )}\n          >\n            {showLineNumbers && (\n              <span className=\"w-10 px-2 text-right text-muted-foreground text-xs border-r border-border shrink-0\">\n                {line.newLineNum ?? \"\"}\n              </span>\n            )}\n            <span\n              className={cn(\n                \"w-6 shrink-0 text-center\",\n                line.type === \"added\" && \"text-green-600 dark:text-green-400\",\n              )}\n            >\n              {line.type === \"added\" ? \"+\" : \" \"}\n            </span>\n            <span\n              className=\"flex-1 px-2\"\n              dangerouslySetInnerHTML={{\n                __html:\n                  highlightedLines.get(leftLines.length + idx) ??\n                  line.content ??\n                  \"\",\n              }}\n            />\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport function CodeDiffViewer({\n  oldCode,\n  newCode,\n  language = \"typescript\",\n  mode = \"unified\",\n  showLineNumbers = true,\n  className,\n}: CodeDiffViewerProps) {\n  const diff = React.useMemo(\n    () => computeDiff(oldCode, newCode),\n    [oldCode, newCode],\n  );\n\n  return (\n    <div\n      data-slot=\"code-diff-viewer\"\n      role=\"region\"\n      aria-label=\"Code diff\"\n      className={cn(\n        \"border border-border rounded-lg overflow-hidden\",\n        className,\n      )}\n    >\n      {mode === \"unified\" ? (\n        <UnifiedDiff\n          diff={diff}\n          language={language}\n          showLineNumbers={showLineNumbers}\n        />\n      ) : (\n        <SplitDiff\n          diff={diff}\n          language={language}\n          showLineNumbers={showLineNumbers}\n        />\n      )}\n    </div>\n  );\n}\n\nexport type { CodeDiffViewerProps, DiffMode };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Unified or split view modes, line numbers, and Shiki-powered syntax highlighting for any language.",
  "categories": [
    "devtools"
  ],
  "type": "registry:ui"
}