{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/diff-view",
  "title": "Diff View",
  "description": "Unified, split, and inline diff viewer with LCS-based diff algorithm and line numbers",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/diff-view.tsx",
      "content": "import { useIsScreenReaderEnabled, Box, Text } from \"ink\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\n\nexport type DiffMode = \"unified\" | \"split\" | \"inline\";\n\nexport interface DiffViewProps {\n  oldText: string;\n  newText: string;\n  filename?: string;\n  language?: string;\n  mode?: DiffMode;\n  context?: number;\n  showLineNumbers?: boolean;\n  accessibleSummary?: string;\n  \"aria-label\"?: string;\n}\n\ninterface DiffOp {\n  type: \"equal\" | \"insert\" | \"delete\";\n  line: string;\n}\n\nconst computeDiff = (oldLines: string[], newLines: string[]): DiffOp[] => {\n  const m = oldLines.length;\n  const n = newLines.length;\n\n  const dp: number[][] = Array.from({ length: m + 1 }, () =>\n    Array.from({ length: n + 1 }, () => 0)\n  );\n  for (let i = 1; i <= m; i += 1) {\n    for (let j = 1; j <= n; j += 1) {\n      dp[i][j] =\n        oldLines[i - 1] === newLines[j - 1]\n          ? (dp[i - 1]?.[j - 1] ?? 0) + 1\n          : Math.max(dp[i - 1]?.[j] ?? 0, dp[i]?.[j - 1] ?? 0);\n    }\n  }\n\n  const ops: DiffOp[] = [];\n  let i = m;\n  let j = n;\n  while (i > 0 || j > 0) {\n    if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {\n      ops.unshift({ line: oldLines[i - 1] ?? \"\", type: \"equal\" });\n      i -= 1;\n      j -= 1;\n    } else if (\n      j > 0 &&\n      (i === 0 || (dp[i]?.[j - 1] ?? 0) >= (dp[i - 1]?.[j] ?? 0))\n    ) {\n      ops.unshift({ line: newLines[j - 1] ?? \"\", type: \"insert\" });\n      j -= 1;\n    } else {\n      ops.unshift({ line: oldLines[i - 1] ?? \"\", type: \"delete\" });\n      i -= 1;\n    }\n  }\n\n  return ops;\n};\n\ninterface Hunk {\n  oldStart: number;\n  newStart: number;\n  ops: DiffOp[];\n}\n\nconst buildHunks = (ops: DiffOp[], context: number): Hunk[] => {\n  let oldLine = 1;\n  let newLine = 1;\n  const numbered = ops.map((op) => {\n    const o = op.type === \"insert\" ? null : oldLine;\n    const n = op.type === \"delete\" ? null : newLine;\n    if (op.type !== \"insert\") {\n      oldLine += 1;\n    }\n    if (op.type !== \"delete\") {\n      newLine += 1;\n    }\n    return { ...op, newLine: n, oldLine: o };\n  });\n\n  const changed = new Set<number>();\n  for (const [idx, op] of numbered.entries()) {\n    if (op.type !== \"equal\") {\n      changed.add(idx);\n    }\n  }\n\n  if (changed.size === 0) {\n    return [];\n  }\n\n  const included = new Set<number>();\n  for (const idx of changed) {\n    for (let d = -context; d <= context; d += 1) {\n      const t = idx + d;\n      if (t >= 0 && t < numbered.length) {\n        included.add(t);\n      }\n    }\n  }\n\n  const indices = [...included].toSorted((a, b) => a - b);\n  const hunks: Hunk[] = [];\n  let start = 0;\n  while (start < indices.length) {\n    let end = start;\n    while (end + 1 < indices.length) {\n      const nextIdx = indices[end + 1] ?? 0;\n      const curIdx = indices[end] ?? 0;\n      if (nextIdx !== curIdx + 1) {\n        break;\n      }\n      end += 1;\n    }\n    const slice = indices.slice(start, end + 1).map((i) => numbered[i]);\n    const firstOld = slice.find((op) => op.oldLine !== null)?.oldLine ?? 1;\n    const firstNew = slice.find((op) => op.newLine !== null)?.newLine ?? 1;\n    hunks.push({ newStart: firstNew, oldStart: firstOld, ops: slice });\n    start = end + 1;\n  }\n\n  return hunks;\n};\n\ninterface ViewProps {\n  hunks: Hunk[];\n  separator: string;\n  showLineNumbers: boolean;\n  theme: ReturnType<typeof useTheme>;\n}\n\nconst UnifiedView = ({ hunks, showLineNumbers, theme }: ViewProps) => {\n  const rows: React.ReactNode[] = [];\n\n  for (const hunk of hunks) {\n    const oldCount = hunk.ops.filter((op) => op.type !== \"insert\").length;\n    const newCount = hunk.ops.filter((op) => op.type !== \"delete\").length;\n    rows.push(\n      <Box key={`hunk-${hunk.oldStart}-${hunk.newStart}`}>\n        <Text color=\"cyan\" dimColor>\n          @@ -{hunk.oldStart},{oldCount} +{hunk.newStart},{newCount} @@\n        </Text>\n      </Box>\n    );\n\n    let ol = hunk.oldStart;\n    let nl = hunk.newStart;\n\n    for (const op of hunk.ops) {\n      const currentOl = op.type === \"insert\" ? null : ol;\n      const currentNl = op.type === \"delete\" ? null : nl;\n      if (op.type !== \"insert\") {\n        ol += 1;\n      }\n      if (op.type !== \"delete\") {\n        nl += 1;\n      }\n\n      const key = `${op.type}-${currentOl ?? \"x\"}-${currentNl ?? \"x\"}`;\n\n      if (op.type === \"delete\") {\n        rows.push(\n          <Box key={key} gap={1}>\n            {showLineNumbers && (\n              <Text color={theme.colors.mutedForeground} dimColor>\n                {String(currentOl ?? \"\").padStart(4)} {\" \".repeat(4)}\n              </Text>\n            )}\n            <Text color=\"red\">-{op.line}</Text>\n          </Box>\n        );\n      } else if (op.type === \"insert\") {\n        rows.push(\n          <Box key={key} gap={1}>\n            {showLineNumbers && (\n              <Text color={theme.colors.mutedForeground} dimColor>\n                {\" \".repeat(4)} {String(currentNl ?? \"\").padStart(4)}\n              </Text>\n            )}\n            <Text color=\"green\">+{op.line}</Text>\n          </Box>\n        );\n      } else {\n        rows.push(\n          <Box key={key} gap={1}>\n            {showLineNumbers && (\n              <Text color={theme.colors.mutedForeground} dimColor>\n                {String(currentOl ?? \"\").padStart(4)}{\" \"}\n                {String(currentNl ?? \"\").padStart(4)}\n              </Text>\n            )}\n            <Text dimColor> {op.line}</Text>\n          </Box>\n        );\n      }\n    }\n  }\n\n  return <Box flexDirection=\"column\">{rows}</Box>;\n};\n\nconst SplitView = ({ hunks, separator, showLineNumbers, theme }: ViewProps) => {\n  const rows: React.ReactNode[] = [];\n\n  for (const hunk of hunks) {\n    const oldCount = hunk.ops.filter((op) => op.type !== \"insert\").length;\n    const newCount = hunk.ops.filter((op) => op.type !== \"delete\").length;\n    rows.push(\n      <Box key={`hunk-${hunk.oldStart}-${hunk.newStart}`}>\n        <Text color=\"cyan\" dimColor>\n          @@ -{hunk.oldStart},{oldCount} +{hunk.newStart},{newCount} @@\n        </Text>\n      </Box>\n    );\n\n    let ol = hunk.oldStart;\n    let nl = hunk.newStart;\n\n    for (const op of hunk.ops) {\n      const currentOl = op.type === \"insert\" ? null : ol;\n      const currentNl = op.type === \"delete\" ? null : nl;\n      if (op.type !== \"insert\") {\n        ol += 1;\n      }\n      if (op.type !== \"delete\") {\n        nl += 1;\n      }\n\n      const key = `${op.type}-${currentOl ?? \"x\"}-${currentNl ?? \"x\"}`;\n\n      if (op.type === \"equal\") {\n        rows.push(\n          <Box key={key} gap={2}>\n            {showLineNumbers && (\n              <Text dimColor color={theme.colors.mutedForeground}>\n                {String(currentOl ?? \"\").padStart(4)}\n              </Text>\n            )}\n            <Text dimColor>{op.line}</Text>\n            <Text> {separator} </Text>\n            {showLineNumbers && (\n              <Text dimColor color={theme.colors.mutedForeground}>\n                {String(currentNl ?? \"\").padStart(4)}\n              </Text>\n            )}\n            <Text dimColor>{op.line}</Text>\n          </Box>\n        );\n      } else if (op.type === \"delete\") {\n        rows.push(\n          <Box key={key} gap={2}>\n            {showLineNumbers && (\n              <Text dimColor color={theme.colors.mutedForeground}>\n                {String(currentOl ?? \"\").padStart(4)}\n              </Text>\n            )}\n            <Text color=\"red\">-{op.line}</Text>\n            <Text> {separator} </Text>\n            <Text> </Text>\n          </Box>\n        );\n      } else {\n        rows.push(\n          <Box key={key} gap={2}>\n            <Text> </Text>\n            <Text> {separator} </Text>\n            {showLineNumbers && (\n              <Text dimColor color={theme.colors.mutedForeground}>\n                {String(currentNl ?? \"\").padStart(4)}\n              </Text>\n            )}\n            <Text color=\"green\">+{op.line}</Text>\n          </Box>\n        );\n      }\n    }\n  }\n\n  return <Box flexDirection=\"column\">{rows}</Box>;\n};\n\ninterface InlineViewProps {\n  ops: DiffOp[];\n  showLineNumbers: boolean;\n  theme: ReturnType<typeof useTheme>;\n}\n\nconst InlineView = ({ ops, showLineNumbers, theme }: InlineViewProps) => {\n  const rows: React.ReactNode[] = [];\n  let oldLine = 1;\n  let newLine = 1;\n\n  for (const op of ops) {\n    const currentOl = op.type === \"insert\" ? null : oldLine;\n    const currentNl = op.type === \"delete\" ? null : newLine;\n    if (op.type !== \"insert\") {\n      oldLine += 1;\n    }\n    if (op.type !== \"delete\") {\n      newLine += 1;\n    }\n\n    const key = `${op.type}-${currentOl ?? \"x\"}-${currentNl ?? \"x\"}`;\n\n    if (op.type === \"delete\") {\n      rows.push(\n        <Box key={key} gap={1}>\n          {showLineNumbers && (\n            <Text color={theme.colors.mutedForeground} dimColor>\n              {String(currentOl ?? \"\").padStart(4)} {\"    \"}\n            </Text>\n          )}\n          <Text color=\"red\" dimColor>\n            -{op.line}\n          </Text>\n        </Box>\n      );\n    } else if (op.type === \"insert\") {\n      rows.push(\n        <Box key={key} gap={1}>\n          {showLineNumbers && (\n            <Text color={theme.colors.mutedForeground} dimColor>\n              {\"    \"} {String(currentNl ?? \"\").padStart(4)}\n            </Text>\n          )}\n          <Text color=\"green\">+{op.line}</Text>\n        </Box>\n      );\n    } else {\n      rows.push(\n        <Box key={key} gap={1}>\n          {showLineNumbers && (\n            <Text color={theme.colors.mutedForeground} dimColor>\n              {String(currentOl ?? \"\").padStart(4)}{\" \"}\n              {String(currentNl ?? \"\").padStart(4)}\n            </Text>\n          )}\n          <Text dimColor> {op.line}</Text>\n        </Box>\n      );\n    }\n  }\n\n  return <Box flexDirection=\"column\">{rows}</Box>;\n};\n\nexport const DiffView = ({\n  oldText,\n  newText,\n  filename,\n  mode = \"unified\",\n  context = 3,\n  showLineNumbers = false,\n  accessibleSummary,\n  \"aria-label\": ariaLabel,\n}: DiffViewProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const isScreenReaderEnabled = useIsScreenReaderEnabled();\n\n  const oldLines = oldText.split(\"\\n\");\n  const newLines = newText.split(\"\\n\");\n  const ops = computeDiff(oldLines, newLines);\n  const hunks = buildHunks(ops, context);\n  const hasChanges = ops.some((op) => op.type !== \"equal\");\n  const changes = (() => {\n    let oldLine = 1;\n    let newLine = 1;\n    return ops.flatMap((operation) => {\n      const currentOldLine = operation.type === \"insert\" ? undefined : oldLine;\n      const currentNewLine = operation.type === \"delete\" ? undefined : newLine;\n      if (operation.type !== \"insert\") {\n        oldLine += 1;\n      }\n      if (operation.type !== \"delete\") {\n        newLine += 1;\n      }\n      return operation.type === \"equal\"\n        ? []\n        : [{ ...operation, newLine: currentNewLine, oldLine: currentOldLine }];\n    });\n  })();\n\n  if (!hasChanges) {\n    return (\n      <Box flexDirection=\"column\">\n        {filename && (\n          <Text bold color={theme.colors.foreground}>\n            {filename}\n          </Text>\n        )}\n        <Text dimColor color={theme.colors.mutedForeground}>\n          No differences\n        </Text>\n      </Box>\n    );\n  }\n\n  if (isScreenReaderEnabled) {\n    return (\n      <Box flexDirection=\"column\" aria-role=\"list\">\n        <Text\n          aria-label={\n            ariaLabel ??\n            accessibleSummary ??\n            `Diff for ${filename ?? \"text\"}. ${changes.length} changed lines.`\n          }\n        >\n          {\"\"}\n        </Text>\n        {changes.slice(0, 200).map((change, index) => (\n          <Box\n            key={`${change.type}-${change.oldLine ?? \"x\"}-${change.newLine ?? \"x\"}-${index}`}\n            aria-role=\"listitem\"\n          >\n            <Text>\n              {change.type === \"insert\"\n                ? `Added at new line ${change.newLine}: ${change.line}`\n                : `Removed at old line ${change.oldLine}: ${change.line}`}\n            </Text>\n          </Box>\n        ))}\n        {changes.length > 200 && (\n          <Text>{`${changes.length - 200} additional changed lines omitted.`}</Text>\n        )}\n      </Box>\n    );\n  }\n\n  let content: React.ReactNode;\n  if (mode === \"split\") {\n    content = (\n      <SplitView\n        hunks={hunks}\n        separator={unicode ? \"│\" : \"|\"}\n        showLineNumbers={showLineNumbers}\n        theme={theme}\n      />\n    );\n  } else if (mode === \"inline\") {\n    content = (\n      <InlineView ops={ops} showLineNumbers={showLineNumbers} theme={theme} />\n    );\n  } else {\n    content = (\n      <UnifiedView\n        hunks={hunks}\n        separator={unicode ? \"│\" : \"|\"}\n        showLineNumbers={showLineNumbers}\n        theme={theme}\n      />\n    );\n  }\n\n  return (\n    <Box flexDirection=\"column\" aria-label={ariaLabel ?? accessibleSummary}>\n      {filename && (\n        <Text bold color={theme.colors.foreground}>\n          --- {filename}\n        </Text>\n      )}\n      {content}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}