{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "file-diff",
  "title": "File Diff",
  "description": "A change an agent proposes to a file, with accept and reject.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.cuelark.com/r/theme.json",
    "https://ui.cuelark.com/r/button.json",
    "https://ui.cuelark.com/r/motion.json"
  ],
  "files": [
    {
      "path": "src/registry/cuelark/ui/file-diff.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, FileCodeIcon, XIcon } from \"lucide-react\";\n\nimport { enterChildren, motionScale } from \"@/lib/motion\";\nimport { Button } from \"@/registry/cuelark/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ntype DiffLine = { type: \"add\" | \"remove\" | \"context\"; content: string };\n\ntype FileDiffProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  filename: string;\n  lines: DiffLine[];\n  onAccept?: () => void;\n  onReject?: () => void;\n  /** Unchanged runs longer than this fold into \"N unchanged lines\". */\n  foldAfter?: number;\n};\n\n/**\n * A change an agent proposes to a file: added and removed lines with gutters, +/− stats, long unchanged runs folded\n * away, and Accept/Reject. Once decided, the header records the outcome and the actions step aside.\n */\nfunction FileDiff({ filename, lines, onAccept, onReject, foldAfter = 6, className, ...props }: FileDiffProps) {\n  const [decision, setDecision] = React.useState<\"accepted\" | \"rejected\" | null>(null);\n  const [unfolded, setUnfolded] = React.useState<Set<number>>(new Set());\n  const body = React.useRef<HTMLTableSectionElement>(null);\n  React.useLayoutEffect(() => {\n    const el = body.current;\n    if (el && motionScale(el)) enterChildren(el.querySelectorAll(\"tr.bg-success\\\\/10, tr.bg-destructive\\\\/10\"), { opacity: 0, x: -6 }, { stagger: 0.03, duration: 0.3 });\n  }, []);\n  const added = lines.filter((l) => l.type === \"add\").length;\n  const removed = lines.filter((l) => l.type === \"remove\").length;\n\n  // Number lines on both sides and group long context runs.\n  let oldNo = 1;\n  let newNo = 1;\n  const rows = lines.map((l) => {\n    const row = { ...l, old: l.type === \"add\" ? null : oldNo, new: l.type === \"remove\" ? null : newNo };\n    if (l.type !== \"add\") oldNo++;\n    if (l.type !== \"remove\") newNo++;\n    return row;\n  });\n  const blocks: ({ fold: number; start: number } | (typeof rows)[number])[] = [];\n  for (let i = 0; i < rows.length; ) {\n    let j = i;\n    while (j < rows.length && rows[j]!.type === \"context\") j++;\n    const run = j - i;\n    if (run > foldAfter && !unfolded.has(i)) {\n      blocks.push(...rows.slice(i, i + 2), { fold: run - 4, start: i }, ...rows.slice(j - 2, j));\n      i = j;\n    } else if (run > 0) {\n      blocks.push(...rows.slice(i, j));\n      i = j;\n    } else {\n      blocks.push(rows[i]!);\n      i++;\n    }\n  }\n\n  return (\n    <div data-slot=\"file-diff\" data-decision={decision ?? undefined} className={cn(\"overflow-hidden rounded-xl border border-border bg-card text-sm\", className)} {...props}>\n      <div className=\"flex items-center gap-2.5 border-b border-border px-3 py-2\">\n        <FileCodeIcon className=\"size-4 text-muted-foreground\" />\n        <span className=\"truncate font-mono text-[0.8125rem]\">{filename}</span>\n        <span className=\"font-mono text-xs\">\n          <span className=\"text-success\">+{added}</span> <span className=\"text-destructive\">−{removed}</span>\n        </span>\n        <span className=\"ms-auto flex items-center gap-1.5\">\n          {decision ? (\n            <span className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n              {decision === \"accepted\" ? <CheckIcon className=\"size-3.5 text-success\" /> : <XIcon className=\"size-3.5\" />}\n              {decision === \"accepted\" ? \"Accepted\" : \"Rejected\"}\n            </span>\n          ) : (\n            <>\n              <Button variant=\"ghost\" size=\"xs\" onClick={() => (setDecision(\"rejected\"), onReject?.())}>\n                Reject\n              </Button>\n              <Button size=\"xs\" onClick={() => (setDecision(\"accepted\"), onAccept?.())}>\n                Accept\n              </Button>\n            </>\n          )}\n        </span>\n      </div>\n      <div className={cn(\"overflow-x-auto font-mono text-xs leading-6 transition-opacity\", decision === \"rejected\" && \"opacity-50\")}>\n        <table className=\"w-full border-collapse\">\n          <tbody ref={body}>\n            {blocks.map((b, i) =>\n              \"fold\" in b ? (\n                <tr key={`fold-${i}`}>\n                  <td colSpan={3}>\n                    <button type=\"button\" onClick={() => setUnfolded((s) => new Set(s).add(b.start))} className=\"w-full bg-muted/40 py-1 text-center text-[0.6875rem] text-muted-foreground hover:bg-muted hover:text-foreground\">\n                      ⋯ {b.fold} unchanged lines\n                    </button>\n                  </td>\n                </tr>\n              ) : (\n                <tr key={i} className={cn(b.type === \"add\" && \"bg-success/10\", b.type === \"remove\" && \"bg-destructive/10\")}>\n                  <td className=\"w-10 border-e border-border pe-2 text-right text-muted-foreground/70 select-none\">{b.old ?? \"\"}</td>\n                  <td className=\"w-10 border-e border-border pe-2 text-right text-muted-foreground/70 select-none\">{b.new ?? \"\"}</td>\n                  <td className=\"ps-2 pe-4 whitespace-pre\">\n                    <span className={cn(\"me-2 inline-block w-2 select-none\", b.type === \"add\" ? \"text-success\" : b.type === \"remove\" ? \"text-destructive\" : \"text-transparent\")}>\n                      {b.type === \"add\" ? \"+\" : b.type === \"remove\" ? \"−\" : \" \"}\n                    </span>\n                    {b.content}\n                  </td>\n                </tr>\n              ),\n            )}\n          </tbody>\n        </table>\n      </div>\n    </div>\n  );\n}\n\nexport { FileDiff };\nexport type { FileDiffProps, DiffLine };\n",
      "type": "registry:ui"
    }
  ],
  "docs": "Docs and examples: https://ui.cuelark.com/components/file-diff",
  "categories": [
    "agents"
  ],
  "type": "registry:ui"
}