{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opentui/diff-view",
  "title": "Diff View",
  "description": "Unified, split, and inline diff viewer with LCS-based diff algorithm and line numbers",
  "dependencies": [
    "@opentui/react"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/opentui/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/opentui/ui/diff-view.tsx",
      "content": "/* @jsxImportSource @opentui/react */\nimport type { ReactNode } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\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}\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  showLineNumbers: boolean;\n  theme: ReturnType<typeof useTheme>;\n}\n\nconst UnifiedView = ({ hunks, showLineNumbers, theme }: ViewProps) => {\n  const rows: 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 fg=\"cyan\">{`@@ -${hunk.oldStart},${oldCount} +${hunk.newStart},${newCount} @@`}</text>\n      </box>\n    );\n    let ol = hunk.oldStart;\n    let nl = hunk.newStart;\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      const key = `${op.type}-${currentOl ?? \"x\"}-${currentNl ?? \"x\"}`;\n      if (op.type === \"delete\") {\n        rows.push(\n          <box gap={1}>\n            {showLineNumbers && (\n              <text\n                fg={theme.colors.mutedForeground}\n              >{`${String(currentOl ?? \"\").padStart(4)} ${\"    \"}`}</text>\n            )}\n            <text fg=\"red\">{`-${op.line}`}</text>\n          </box>\n        );\n      } else if (op.type === \"insert\") {\n        rows.push(\n          <box gap={1}>\n            {showLineNumbers && (\n              <text\n                fg={theme.colors.mutedForeground}\n              >{`${\"    \"} ${String(currentNl ?? \"\").padStart(4)}`}</text>\n            )}\n            <text fg=\"green\">{`+${op.line}`}</text>\n          </box>\n        );\n      } else {\n        rows.push(\n          <box gap={1}>\n            {showLineNumbers && (\n              <text\n                fg={theme.colors.mutedForeground}\n              >{`${String(currentOl ?? \"\").padStart(4)} ${String(currentNl ?? \"\").padStart(4)}`}</text>\n            )}\n            <text fg=\"#666\">{` ${op.line}`}</text>\n          </box>\n        );\n      }\n    }\n  }\n\n  return <box flexDirection=\"column\">{rows}</box>;\n};\n\nconst SplitView = ({ hunks, showLineNumbers, theme }: ViewProps) => {\n  const rows: 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 fg=\"cyan\">{`@@ -${hunk.oldStart},${oldCount} +${hunk.newStart},${newCount} @@`}</text>\n      </box>\n    );\n    let ol = hunk.oldStart;\n    let nl = hunk.newStart;\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      const key = `${op.type}-${currentOl ?? \"x\"}-${currentNl ?? \"x\"}`;\n      if (op.type === \"equal\") {\n        rows.push(\n          <box gap={2}>\n            {showLineNumbers && (\n              <text fg={theme.colors.mutedForeground}>\n                {String(currentOl ?? \"\").padStart(4)}\n              </text>\n            )}\n            <text fg=\"#666\">{op.line}</text>\n            <text>{\" │ \"}</text>\n            {showLineNumbers && (\n              <text fg={theme.colors.mutedForeground}>\n                {String(currentNl ?? \"\").padStart(4)}\n              </text>\n            )}\n            <text fg=\"#666\">{op.line}</text>\n          </box>\n        );\n      } else if (op.type === \"delete\") {\n        rows.push(\n          <box gap={2}>\n            {showLineNumbers && (\n              <text fg={theme.colors.mutedForeground}>\n                {String(currentOl ?? \"\").padStart(4)}\n              </text>\n            )}\n            <text fg=\"red\">{`-${op.line}`}</text>\n            <text>{\" │ \"}</text>\n            <text> </text>\n          </box>\n        );\n      } else {\n        rows.push(\n          <box gap={2}>\n            <text> </text>\n            <text>{\" │ \"}</text>\n            {showLineNumbers && (\n              <text fg={theme.colors.mutedForeground}>\n                {String(currentNl ?? \"\").padStart(4)}\n              </text>\n            )}\n            <text fg=\"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: 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    const key = `${op.type}-${currentOl ?? \"x\"}-${currentNl ?? \"x\"}`;\n    if (op.type === \"delete\") {\n      rows.push(\n        <box gap={1}>\n          {showLineNumbers && (\n            <text\n              fg={theme.colors.mutedForeground}\n            >{`${String(currentOl ?? \"\").padStart(4)} ${\"    \"}`}</text>\n          )}\n          <text fg=\"red\">{`-${op.line}`}</text>\n        </box>\n      );\n    } else if (op.type === \"insert\") {\n      rows.push(\n        <box gap={1}>\n          {showLineNumbers && (\n            <text\n              fg={theme.colors.mutedForeground}\n            >{`${\"    \"} ${String(currentNl ?? \"\").padStart(4)}`}</text>\n          )}\n          <text fg=\"green\">{`+${op.line}`}</text>\n        </box>\n      );\n    } else {\n      rows.push(\n        <box gap={1}>\n          {showLineNumbers && (\n            <text\n              fg={theme.colors.mutedForeground}\n            >{`${String(currentOl ?? \"\").padStart(4)} ${String(currentNl ?? \"\").padStart(4)}`}</text>\n          )}\n          <text fg=\"#666\">{` ${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}: DiffViewProps) => {\n  const theme = useTheme();\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\n  if (!hasChanges) {\n    return (\n      <box flexDirection=\"column\">\n        {filename && (\n          <text fg={theme.colors.foreground}>\n            <b>{filename}</b>\n          </text>\n        )}\n        <text fg={theme.colors.mutedForeground}>No differences</text>\n      </box>\n    );\n  }\n\n  let content: ReactNode;\n  if (mode === \"split\") {\n    content = (\n      <SplitView\n        hunks={hunks}\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        showLineNumbers={showLineNumbers}\n        theme={theme}\n      />\n    );\n  }\n\n  return (\n    <box flexDirection=\"column\">\n      {filename && (\n        <text fg={theme.colors.foreground}>\n          <b>{`--- ${filename}`}</b>\n        </text>\n      )}\n      {content}\n    </box>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/diff-view.tsx"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:ui"
}