{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-viewer",
  "type": "registry:ui",
  "title": "Diff Viewer",
  "description": "Render code diffs with highlighted additions and deletions.",
  "dependencies": [
    "diff",
    "parse-diff",
    "@assistant-ui/react-markdown",
    "class-variance-authority"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/diff-viewer.tsx",
      "content": "\"use client\";\n\nimport { type ComponentProps, useMemo } from \"react\";\nimport type { SyntaxHighlighterProps } from \"@assistant-ui/react-markdown\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { diffLines } from \"diff\";\nimport parseDiff from \"parse-diff\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype DiffLineType = \"add\" | \"del\" | \"normal\";\n\ninterface ParsedLine {\n  type: DiffLineType;\n  content: string;\n  oldLineNumber?: number;\n  newLineNumber?: number;\n}\n\ninterface ParsedFile {\n  oldName?: string | undefined;\n  newName?: string | undefined;\n  lines: ParsedLine[];\n  additions: number;\n  deletions: number;\n}\n\ninterface SplitLinePair {\n  left: ParsedLine | null;\n  right: ParsedLine | null;\n}\n\nfunction parsePatch(patch: string): ParsedFile[] {\n  const files = parseDiff(patch);\n  return files.map((file) => {\n    const lines: ParsedLine[] = [];\n    let additions = 0;\n    let deletions = 0;\n    for (const chunk of file.chunks) {\n      let oldLine = chunk.oldStart;\n      let newLine = chunk.newStart;\n      for (const change of chunk.changes) {\n        if (change.type === \"add\") {\n          additions++;\n          lines.push({\n            type: \"add\",\n            content: change.content.slice(1),\n            newLineNumber: newLine++,\n          });\n        } else if (change.type === \"del\") {\n          deletions++;\n          lines.push({\n            type: \"del\",\n            content: change.content.slice(1),\n            oldLineNumber: oldLine++,\n          });\n        } else {\n          lines.push({\n            type: \"normal\",\n            content: change.content.slice(1),\n            oldLineNumber: oldLine++,\n            newLineNumber: newLine++,\n          });\n        }\n      }\n    }\n    return {\n      oldName: file.from,\n      newName: file.to,\n      lines,\n      additions,\n      deletions,\n    };\n  });\n}\n\nfunction computeDiff(\n  oldContent: string,\n  newContent: string,\n): { lines: ParsedLine[]; additions: number; deletions: number } {\n  const changes = diffLines(oldContent, newContent);\n  const lines: ParsedLine[] = [];\n  let oldLine = 1;\n  let newLine = 1;\n  let additions = 0;\n  let deletions = 0;\n\n  for (const change of changes) {\n    const contentLines = change.value.replace(/\\n$/, \"\").split(\"\\n\");\n    for (const content of contentLines) {\n      if (change.added) {\n        additions++;\n        lines.push({ type: \"add\", content, newLineNumber: newLine++ });\n      } else if (change.removed) {\n        deletions++;\n        lines.push({ type: \"del\", content, oldLineNumber: oldLine++ });\n      } else {\n        lines.push({\n          type: \"normal\",\n          content,\n          oldLineNumber: oldLine++,\n          newLineNumber: newLine++,\n        });\n      }\n    }\n  }\n  return { lines, additions, deletions };\n}\n\nfunction pairLinesForSplit(lines: ParsedLine[]): SplitLinePair[] {\n  const pairs: SplitLinePair[] = [];\n  let i = 0;\n\n  while (i < lines.length) {\n    const line = lines[i]!;\n    if (line.type === \"normal\") {\n      pairs.push({ left: line, right: line });\n      i++;\n    } else if (line.type === \"del\") {\n      const deletions: ParsedLine[] = [];\n      while (i < lines.length && lines[i]!.type === \"del\") {\n        deletions.push(lines[i]!);\n        i++;\n      }\n      const additions: ParsedLine[] = [];\n      while (i < lines.length && lines[i]!.type === \"add\") {\n        additions.push(lines[i]!);\n        i++;\n      }\n      const maxLen = Math.max(deletions.length, additions.length);\n      for (let j = 0; j < maxLen; j++) {\n        pairs.push({\n          left: deletions[j] ?? null,\n          right: additions[j] ?? null,\n        });\n      }\n    } else {\n      pairs.push({ left: null, right: line });\n      i++;\n    }\n  }\n  return pairs;\n}\n\nconst diffViewerVariants = cva(\n  \"aui-diff-viewer overflow-hidden font-mono leading-relaxed [font-variant-ligatures:none]\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-foreground/10 bg-foreground/[0.025] dark:bg-foreground/[0.04] border\",\n        ghost: \"bg-transparent\",\n        muted:\n          \"border-foreground/10 bg-foreground/[0.06] dark:bg-foreground/[0.08] border\",\n      },\n      size: {\n        sm: \"text-[11px]\",\n        default: \"text-[12.5px]\",\n        lg: \"text-[13.5px]\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst diffLineVariants = cva(\"flex\", {\n  variants: {\n    type: {\n      add: \"bg-[var(--diff-add-bg,var(--_diff-add-bg))] shadow-[inset_2px_0_0_var(--diff-add-rule,var(--color-green-500))] [--_diff-add-bg:color-mix(in_oklab,var(--color-green-500)_8%,transparent)] dark:[--_diff-add-bg:color-mix(in_oklab,var(--color-green-500)_15%,transparent)]\",\n      del: \"bg-[var(--diff-del-bg,var(--_diff-del-bg))] shadow-[inset_2px_0_0_var(--diff-del-rule,var(--color-red-500))] [--_diff-del-bg:color-mix(in_oklab,var(--color-red-500)_8%,transparent)] dark:[--_diff-del-bg:color-mix(in_oklab,var(--color-red-500)_15%,transparent)]\",\n      normal: \"\",\n      empty: \"\",\n    },\n  },\n  defaultVariants: {\n    type: \"normal\",\n  },\n});\n\nconst diffLineTextVariants = cva(\"\", {\n  variants: {\n    type: {\n      add: \"text-[var(--diff-add-text,var(--color-green-600))] dark:text-[var(--diff-add-text-dark,var(--color-green-400))]\",\n      del: \"text-[var(--diff-del-text,var(--color-red-600))] dark:text-[var(--diff-del-text-dark,var(--color-red-400))]\",\n      normal: \"\",\n      empty: \"\",\n    },\n  },\n  defaultVariants: {\n    type: \"normal\",\n  },\n});\n\nfunction getFileExtension(filename?: string): string {\n  const ext = filename?.split(\".\").pop()?.toLowerCase();\n  if (!ext) return \"\";\n  return ext.toUpperCase();\n}\n\nfunction DiffViewerFileBadge({ filename }: { filename?: string | undefined }) {\n  const ext = getFileExtension(filename);\n  if (!ext) return null;\n\n  return (\n    <span\n      data-slot=\"diff-viewer-file-badge\"\n      className=\"border-foreground/15 text-muted-foreground/70 inline-flex h-4 shrink-0 items-center border px-1 text-[9px] leading-none font-medium tracking-wide\"\n    >\n      {ext}\n    </span>\n  );\n}\n\nfunction DiffViewerStats({\n  additions,\n  deletions,\n}: {\n  additions: number;\n  deletions: number;\n}) {\n  return (\n    <span\n      data-slot=\"diff-viewer-stats\"\n      className=\"flex shrink-0 gap-1.5 text-[11px] tabular-nums\"\n    >\n      <span className=\"text-green-600 dark:text-green-400\">+{additions}</span>\n      <span className=\"text-red-600 dark:text-red-400\">−{deletions}</span>\n    </span>\n  );\n}\n\nfunction DiffViewerFile({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div data-slot=\"diff-viewer-file\" className={cn(className)} {...props} />\n  );\n}\n\nfunction DiffViewerContent({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"diff-viewer-content\"\n      className={cn(\"overflow-x-auto\", className)}\n      {...props}\n    />\n  );\n}\n\ninterface DiffViewerHeaderProps extends ComponentProps<\"div\"> {\n  oldName?: string | undefined;\n  newName?: string | undefined;\n  additions?: number;\n  deletions?: number;\n  showIcon?: boolean;\n  showStats?: boolean;\n}\n\nfunction DiffViewerHeader({\n  oldName,\n  newName,\n  additions = 0,\n  deletions = 0,\n  showIcon = false,\n  showStats = true,\n  className,\n  ...props\n}: DiffViewerHeaderProps) {\n  if (!oldName && !newName) return null;\n\n  const displayName = newName || oldName;\n\n  return (\n    <div\n      data-slot=\"diff-viewer-header\"\n      className={cn(\n        \"border-foreground/10 text-muted-foreground flex h-9 items-center gap-2 border-b ps-3.5 pe-3 text-[11px] font-medium tracking-wide\",\n        className,\n      )}\n      {...props}\n    >\n      {showIcon && <DiffViewerFileBadge filename={displayName} />}\n      <span className=\"min-w-0 flex-1 truncate\">\n        {oldName && newName && oldName !== newName ? (\n          <>\n            <span className=\"text-muted-foreground/60\">{oldName}</span>\n            <span className=\"text-muted-foreground/50\">{\" → \"}</span>\n            <span className=\"text-foreground/80\">{newName}</span>\n          </>\n        ) : (\n          displayName\n        )}\n      </span>\n      {showStats && (additions > 0 || deletions > 0) && (\n        <DiffViewerStats additions={additions} deletions={deletions} />\n      )}\n    </div>\n  );\n}\n\ninterface DiffViewerLineProps extends ComponentProps<\"div\"> {\n  line: ParsedLine;\n  showLineNumbers?: boolean;\n}\n\nfunction DiffViewerLine({\n  line,\n  showLineNumbers = true,\n  className,\n  ...props\n}: DiffViewerLineProps) {\n  const indicator = line.type === \"add\" ? \"+\" : line.type === \"del\" ? \"-\" : \" \";\n\n  return (\n    <div\n      data-slot=\"diff-viewer-line\"\n      data-type={line.type}\n      className={cn(diffLineVariants({ type: line.type }), className)}\n      {...props}\n    >\n      {showLineNumbers && (\n        <span\n          data-slot=\"diff-viewer-line-number\"\n          className=\"text-muted-foreground/40 w-10 shrink-0 px-2 text-end tabular-nums select-none\"\n        >\n          {line.type === \"del\"\n            ? line.oldLineNumber\n            : line.type === \"add\"\n              ? line.newLineNumber\n              : line.oldLineNumber}\n        </span>\n      )}\n      <span\n        data-slot=\"diff-viewer-indicator\"\n        className={cn(\n          \"w-4 shrink-0 text-center select-none\",\n          diffLineTextVariants({ type: line.type }),\n        )}\n      >\n        {indicator}\n      </span>\n      <span\n        data-slot=\"diff-viewer-content\"\n        className=\"flex-1 pe-3.5 break-all whitespace-pre-wrap\"\n      >\n        {line.content}\n      </span>\n    </div>\n  );\n}\n\ninterface DiffViewerSplitLineProps extends ComponentProps<\"div\"> {\n  pair: SplitLinePair;\n  showLineNumbers?: boolean;\n}\n\nfunction DiffViewerSplitLine({\n  pair,\n  showLineNumbers = true,\n  className,\n  ...props\n}: DiffViewerSplitLineProps) {\n  const { left, right } = pair;\n\n  return (\n    <div\n      data-slot=\"diff-viewer-split-line\"\n      className={cn(\"flex\", className)}\n      {...props}\n    >\n      <div\n        data-slot=\"diff-viewer-split-left\"\n        data-type={left?.type ?? \"empty\"}\n        className={cn(\n          \"border-foreground/10 flex w-1/2 border-e\",\n          diffLineVariants({ type: left?.type ?? \"empty\" }),\n        )}\n      >\n        {showLineNumbers && (\n          <span className=\"text-muted-foreground/40 w-10 shrink-0 px-2 text-end tabular-nums select-none\">\n            {left?.oldLineNumber ?? \"\"}\n          </span>\n        )}\n        <span\n          className={cn(\n            \"w-4 shrink-0 text-center select-none\",\n            diffLineTextVariants({ type: left?.type ?? \"empty\" }),\n          )}\n        >\n          {left ? (left.type === \"del\" ? \"-\" : \" \") : \"\"}\n        </span>\n        <span className=\"flex-1 pe-3.5 break-all whitespace-pre-wrap\">\n          {left?.content ?? \"\"}\n        </span>\n      </div>\n      <div\n        data-slot=\"diff-viewer-split-right\"\n        data-type={right?.type ?? \"empty\"}\n        className={cn(\n          \"flex w-1/2\",\n          diffLineVariants({ type: right?.type ?? \"empty\" }),\n        )}\n      >\n        {showLineNumbers && (\n          <span className=\"text-muted-foreground/40 w-10 shrink-0 px-2 text-end tabular-nums select-none\">\n            {right?.newLineNumber ?? \"\"}\n          </span>\n        )}\n        <span\n          className={cn(\n            \"w-4 shrink-0 text-center select-none\",\n            diffLineTextVariants({ type: right?.type ?? \"empty\" }),\n          )}\n        >\n          {right ? (right.type === \"add\" ? \"+\" : \" \") : \"\"}\n        </span>\n        <span className=\"flex-1 pe-3.5 break-all whitespace-pre-wrap\">\n          {right?.content ?? \"\"}\n        </span>\n      </div>\n    </div>\n  );\n}\n\nexport type DiffViewerProps = Partial<SyntaxHighlighterProps> &\n  VariantProps<typeof diffViewerVariants> & {\n    patch?: string;\n    oldFile?: { content: string; name?: string };\n    newFile?: { content: string; name?: string };\n    viewMode?: \"split\" | \"unified\";\n    showLineNumbers?: boolean;\n    showIcon?: boolean;\n    showStats?: boolean;\n    className?: string;\n  };\n\nfunction DiffViewer({\n  code,\n  patch,\n  oldFile,\n  newFile,\n  viewMode = \"unified\",\n  showLineNumbers = true,\n  showIcon = false,\n  showStats = true,\n  variant,\n  size,\n  className,\n}: DiffViewerProps) {\n  const diffPatch = patch ?? code;\n  const oldContent = oldFile?.content;\n  const oldName = oldFile?.name;\n  const newContent = newFile?.content;\n  const newName = newFile?.name;\n\n  const parsedFiles = useMemo<ParsedFile[]>(() => {\n    if (diffPatch) {\n      return parsePatch(diffPatch);\n    }\n    if (oldContent !== undefined && newContent !== undefined) {\n      const { lines, additions, deletions } = computeDiff(\n        oldContent,\n        newContent,\n      );\n      return [\n        {\n          oldName,\n          newName,\n          lines,\n          additions,\n          deletions,\n        },\n      ];\n    }\n    return [];\n  }, [diffPatch, oldContent, oldName, newContent, newName]);\n\n  const splitLinePairs = useMemo<SplitLinePair[][]>(() => {\n    if (viewMode !== \"split\") return [];\n    return parsedFiles.map((file) => pairLinesForSplit(file.lines));\n  }, [parsedFiles, viewMode]);\n\n  if (parsedFiles.length === 0) {\n    return (\n      <pre\n        data-slot=\"diff-viewer\"\n        className={cn(\n          \"border-foreground/10 bg-foreground/[0.025] dark:bg-foreground/[0.04] text-muted-foreground border px-3.5 py-3 font-mono text-xs\",\n          className,\n        )}\n      >\n        No diff content provided\n      </pre>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"diff-viewer\"\n      data-view-mode={viewMode}\n      data-variant={variant ?? \"default\"}\n      data-size={size ?? \"default\"}\n      className={cn(diffViewerVariants({ variant, size }), className)}\n    >\n      {parsedFiles.map((file, fileIndex) => (\n        <div\n          key={fileIndex}\n          data-slot=\"diff-viewer-file\"\n          className=\"border-foreground/10 [contain-intrinsic-size:auto_240px] [content-visibility:auto] not-first:border-t\"\n        >\n          <DiffViewerHeader\n            oldName={file.oldName}\n            newName={file.newName}\n            additions={file.additions}\n            deletions={file.deletions}\n            showIcon={showIcon}\n            showStats={showStats}\n          />\n          <div data-slot=\"diff-viewer-content\" className=\"overflow-x-auto py-2\">\n            {viewMode === \"split\"\n              ? (splitLinePairs[fileIndex] ?? []).map((pair, pairIndex) => (\n                  <DiffViewerSplitLine\n                    key={pairIndex}\n                    pair={pair}\n                    showLineNumbers={showLineNumbers}\n                  />\n                ))\n              : file.lines.map((line, lineIndex) => (\n                  <DiffViewerLine\n                    key={lineIndex}\n                    line={line}\n                    showLineNumbers={showLineNumbers}\n                  />\n                ))}\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nDiffViewer.displayName = \"DiffViewer\";\n\nexport type { ParsedLine, ParsedFile, SplitLinePair };\n\nexport {\n  DiffViewer,\n  DiffViewerFile,\n  DiffViewerHeader,\n  DiffViewerContent,\n  DiffViewerLine,\n  DiffViewerSplitLine,\n  DiffViewerFileBadge,\n  DiffViewerStats,\n  diffViewerVariants,\n  diffLineVariants,\n  diffLineTextVariants,\n  parsePatch,\n  computeDiff,\n};\n"
    }
  ]
}