{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-diff-viewer",
  "description": "Line-level diff viewer with unified and split modes. No external dependencies.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "@lema-ds/skeleton",
    "@lema-ds/badge",
    "@lema-ds/card",
    "@lema-ds/toggle-group",
    "@lema-ds/tokens"
  ],
  "files": [
    {
      "path": "components/ds/ds-diff-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport type DiffMode = \"split\" | \"unified\"\nexport type DiffLineType = \"added\" | \"removed\" | \"unchanged\"\n\nexport interface DiffLine {\n  type: DiffLineType\n  content: string\n  oldLineNo: number | null\n  newLineNo: number | null\n}\n\nexport interface DiffViewerProps extends React.HTMLAttributes<HTMLDivElement> {\n  oldValue: string\n  newValue: string\n  title?: string\n  oldLabel?: string\n  newLabel?: string\n  mode?: DiffMode\n  /** Max lines to display before truncating — 0 = unlimited */\n  maxLines?: number\n  loading?: boolean\n  /** Show line numbers */\n  showLineNumbers?: boolean\n}\n\n// ── Variants ───────────────────────────────────────────────────────────────\n\nexport const diffViewerContainerVariants = cva(\n  \"w-full overflow-hidden rounded-lg border border-border text-xs\",\n  {\n    variants: {},\n  }\n)\n\nexport const diffLineVariants = cva(\"flex gap-2 px-3 py-0.5 leading-5\", {\n  variants: {\n    type: {\n      added: \"bg-success/10 text-success\",\n      removed: \"bg-destructive/10 text-destructive\",\n      unchanged: \"text-foreground\",\n    },\n  },\n  defaultVariants: { type: \"unchanged\" },\n})\n\n// ── Diff algorithm (LCS-based) ────────────────────────────────────────────\n\nfunction computeDiff(oldText: string, newText: string): DiffLine[] {\n  const oldLines = oldText.split(\"\\n\")\n  const newLines = newText.split(\"\\n\")\n  const m = oldLines.length\n  const n = newLines.length\n\n  // Build LCS table\n  const dp: number[][] = Array.from({ length: m + 1 }, () =>\n    new Array(n + 1).fill(0)\n  )\n  for (let i = 1; i <= m; i++) {\n    for (let j = 1; j <= n; j++) {\n      if (oldLines[i - 1] === newLines[j - 1]) {\n        dp[i]![j] = dp[i - 1]![j - 1]! + 1\n      } else {\n        dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!)\n      }\n    }\n  }\n\n  // Backtrack\n  const result: DiffLine[] = []\n  let i = m\n  let j = n\n  let oldNo = m\n  let newNo = n\n\n  while (i > 0 || j > 0) {\n    if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {\n      result.push({\n        type: \"unchanged\",\n        content: oldLines[i - 1]!,\n        oldLineNo: oldNo--,\n        newLineNo: newNo--,\n      })\n      i--\n      j--\n    } else if (j > 0 && (i === 0 || dp[i]![j - 1]! >= dp[i - 1]![j]!)) {\n      result.push({\n        type: \"added\",\n        content: newLines[j - 1]!,\n        oldLineNo: null,\n        newLineNo: newNo--,\n      })\n      j--\n    } else {\n      result.push({\n        type: \"removed\",\n        content: oldLines[i - 1]!,\n        oldLineNo: oldNo--,\n        newLineNo: null,\n      })\n      i--\n    }\n  }\n\n  return result.reverse()\n}\n\n// ── Line Number ───────────────────────────────────────────────────────────\n\nfunction LineNo({ n }: { n: number | null }) {\n  return (\n    <span className=\"w-8 shrink-0 text-right text-muted-foreground opacity-50 select-none\">\n      {n ?? \" \"}\n    </span>\n  )\n}\n\n// ── DiffViewer ────────────────────────────────────────────────────────────\n\nexport function DiffViewer({\n  oldValue,\n  newValue,\n  title,\n  oldLabel = \"Before\",\n  newLabel = \"After\",\n  mode: modeProp = \"unified\",\n  maxLines = 500,\n  loading = false,\n  showLineNumbers = true,\n  className,\n  ...props\n}: DiffViewerProps) {\n  const [mode, setMode] = React.useState<DiffMode>(modeProp)\n\n  // ── Compute diff ─────────────────────────────────────────────────────────\n\n  const diffLines = React.useMemo(\n    () => computeDiff(oldValue, newValue),\n    [oldValue, newValue]\n  )\n\n  // ── Loading ─────────────────────────────────────────────────────────────\n\n  if (loading) {\n    return (\n      <Card className={cn(\"w-full\", className)} {...props}>\n        {title && (\n          <CardHeader className=\"pb-3\">\n            <Skeleton className=\"h-5 w-40\" />\n          </CardHeader>\n        )}\n        <CardContent className=\"p-0\">\n          <div className={diffViewerContainerVariants()}>\n            {Array.from({ length: 6 }).map((_, i) => (\n              <div\n                key={i}\n                className=\"flex gap-2 border-b border-border/50 px-3 py-0.5\"\n              >\n                <Skeleton className=\"h-3 w-8\" />\n                <Skeleton\n                  className=\"h-3\"\n                  style={{ width: `${30 + ((i * 17) % 50)}%` }}\n                />\n              </div>\n            ))}\n          </div>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  const addedCount = diffLines.filter((l) => l.type === \"added\").length\n  const removedCount = diffLines.filter((l) => l.type === \"removed\").length\n  const displayLines = maxLines > 0 ? diffLines.slice(0, maxLines) : diffLines\n  const truncated = maxLines > 0 && diffLines.length > maxLines\n\n  // ── Unified view ─────────────────────────────────────────────────────────\n\n  const unifiedView = (\n    <div\n      className={cn(diffViewerContainerVariants(), \"max-h-120 overflow-auto\")}\n    >\n      {displayLines.map((line, i) => (\n        <div\n          key={i}\n          className={cn(\n            diffLineVariants({ type: line.type }),\n            \"border-b border-border/20\"\n          )}\n        >\n          {showLineNumbers && <LineNo n={line.oldLineNo} />}\n          {showLineNumbers && <LineNo n={line.newLineNo} />}\n          <span className=\"w-3 shrink-0 font-bold select-none\">\n            {line.type === \"added\" ? \"+\" : line.type === \"removed\" ? \"−\" : \" \"}\n          </span>\n          <span className=\"flex-1 whitespace-pre\">{line.content}</span>\n        </div>\n      ))}\n      {truncated && (\n        <div className=\"px-3 py-1 text-xs text-muted-foreground\">\n          … {diffLines.length - maxLines} more lines not shown\n        </div>\n      )}\n    </div>\n  )\n\n  // ── Split view ────────────────────────────────────────────────────────────\n\n  const oldLines = displayLines\n    .filter((l) => l.type !== \"added\")\n    .map((l) => ({ ...l, type: l.type as \"unchanged\" | \"removed\" }))\n  const newLines = displayLines\n    .filter((l) => l.type !== \"removed\")\n    .map((l) => ({ ...l, type: l.type as \"unchanged\" | \"added\" }))\n\n  const splitView = (\n    <div className=\"flex max-h-120 overflow-auto\">\n      {/* Old */}\n      <div\n        className={cn(\n          diffViewerContainerVariants(),\n          \"flex-1 rounded-r-none border-r-0\"\n        )}\n      >\n        <div className=\"border-b border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground\">\n          {oldLabel}\n        </div>\n        {oldLines.map((line, i) => (\n          <div\n            key={i}\n            className={cn(\n              diffLineVariants({ type: line.type }),\n              \"border-b border-border/20\"\n            )}\n          >\n            {showLineNumbers && <LineNo n={line.oldLineNo} />}\n            <span className=\"flex-1 whitespace-pre\">{line.content}</span>\n          </div>\n        ))}\n      </div>\n      {/* New */}\n      <div\n        className={cn(diffViewerContainerVariants(), \"flex-1 rounded-l-none\")}\n      >\n        <div className=\"border-b border-border bg-muted px-3 py-1 text-xs font-medium text-muted-foreground\">\n          {newLabel}\n        </div>\n        {newLines.map((line, i) => (\n          <div\n            key={i}\n            className={cn(\n              diffLineVariants({ type: line.type }),\n              \"border-b border-border/20\"\n            )}\n          >\n            {showLineNumbers && <LineNo n={line.newLineNo} />}\n            <span className=\"flex-1 whitespace-pre\">{line.content}</span>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n\n  return (\n    <Card className={cn(\"w-full\", className)} {...props}>\n      <CardHeader className=\"flex-row items-center justify-between gap-4 pb-3\">\n        <div className=\"flex flex-col gap-1\">\n          {title && <CardTitle className=\"text-sm\">{title}</CardTitle>}\n          <div className=\"flex items-center gap-2\">\n            {addedCount > 0 && (\n              <Badge\n                variant=\"outline\"\n                className=\"border-success/30 text-success\"\n              >\n                +{addedCount}\n              </Badge>\n            )}\n            {removedCount > 0 && (\n              <Badge\n                variant=\"outline\"\n                className=\"border-destructive/30 text-destructive\"\n              >\n                −{removedCount}\n              </Badge>\n            )}\n          </div>\n        </div>\n        <ToggleGroup\n          type=\"single\"\n          size=\"sm\"\n          value={mode}\n          onValueChange={(v) => v && setMode(v as DiffMode)}\n        >\n          <ToggleGroupItem value=\"unified\">Unified</ToggleGroupItem>\n          <ToggleGroupItem value=\"split\">Split</ToggleGroupItem>\n        </ToggleGroup>\n      </CardHeader>\n      <CardContent className=\"p-0 pt-0\">\n        {mode === \"unified\" ? unifiedView : splitView}\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
