{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-review",
  "title": "Diff Review",
  "description": "Accept or reject an agent's edits hunk by hunk, with undo.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://spotcheckui.com/r/spotcheck-base.json",
    "https://spotcheckui.com/r/button.json",
    "https://spotcheckui.com/r/status-pill.json"
  ],
  "files": [
    {
      "path": "registry/spotcheck/components/spotcheck/diff-review.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/spotcheck/components/spotcheck/button\"\nimport { StatusPill } from \"@/registry/spotcheck/components/spotcheck/status-pill\"\n\nconst COLLAPSE = { type: \"spring\", stiffness: 420, damping: 36 } as const\nconst LINE_STYLES = {\n  add: \"border-ok bg-[color-mix(in_srgb,var(--ok)_10%,transparent)]\",\n  remove: \"border-bad bg-[color-mix(in_srgb,var(--bad)_10%,transparent)]\",\n  context: \"border-transparent\",\n}\nconst PREFIXES = { add: \"+\", remove: \"−\", context: \" \" }\nconst SPOKEN = { add: \"Added: \", remove: \"Removed: \", context: \"\" }\n\nexport type DiffLine = { kind: keyof typeof LINE_STYLES; text: string }\nexport type DiffHunk = { id: string; file: string; summary?: string; lines: DiffLine[] }\nexport type HunkDecision = \"accepted\" | \"rejected\"\n\ntype DiffReviewProps = {\n  hunks: DiffHunk[]\n  /** Runs on every decision, with null when one is undone. */\n  onDecide?: (id: string, decision: HunkDecision | null) => void\n  className?: string\n}\n\nexport function reviewCounts(hunks: DiffHunk[], decisions: Record<string, HunkDecision>) {\n  const reviewed = hunks.filter((hunk) => decisions[hunk.id]).length\n  return { reviewed, remaining: hunks.length - reviewed }\n}\n\nexport function DiffReview({ hunks, onDecide, className }: DiffReviewProps) {\n  const [decisions, setDecisions] = React.useState<Record<string, HunkDecision>>({})\n  const reduced = useReducedMotion()\n  const { reviewed, remaining } = reviewCounts(hunks, decisions)\n\n  const decide = (id: string, decision: HunkDecision | null) => {\n    setDecisions((current) => {\n      const next = { ...current }\n      if (decision) next[id] = decision\n      else delete next[id]\n      return next\n    })\n    onDecide?.(id, decision)\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-3 font-(family-name:--f-body)\", className)}>\n      <div className=\"flex flex-wrap items-center justify-between gap-3\">\n        <span role=\"status\" className=\"spotcheck-label\" data-testid=\"review-count\">\n          {reviewed} of {hunks.length} reviewed\n        </span>\n        <Button\n          variant=\"ghost\"\n          disabled={remaining === 0}\n          onClick={() => hunks.filter((hunk) => !decisions[hunk.id]).forEach((hunk) => decide(hunk.id, \"accepted\"))}\n        >\n          Accept {remaining} left\n        </Button>\n      </div>\n      {hunks.map((hunk) => {\n        const decision = decisions[hunk.id]\n        return (\n          <section key={hunk.id} aria-label={hunk.file} className=\"border-ink border-(length:--rule)\">\n            <div className=\"flex flex-wrap items-center justify-between gap-2 px-3 py-2\">\n              <div className=\"min-w-0\">\n                <div className=\"truncate font-(family-name:--f-mono) text-xs font-semibold\">{hunk.file}</div>\n                {hunk.summary && <div className=\"text-mute text-[13px]\">{hunk.summary}</div>}\n              </div>\n              {decision ? (\n                <div className=\"flex items-center gap-3\">\n                  <StatusPill tone={decision === \"accepted\" ? \"ok\" : \"bad\"}>{decision}</StatusPill>\n                  <Button variant=\"ghost\" className=\"px-2 py-1\" onClick={() => decide(hunk.id, null)}>\n                    Undo\n                  </Button>\n                </div>\n              ) : (\n                <div className=\"flex gap-3\">\n                  <Button aria-label={`Reject change to ${hunk.file}`} className=\"px-2.5 py-1.5\" onClick={() => decide(hunk.id, \"rejected\")}>\n                    Reject\n                  </Button>\n                  <Button\n                    variant=\"accent\"\n                    aria-label={`Accept change to ${hunk.file}`}\n                    className=\"px-2.5 py-1.5\"\n                    onClick={() => decide(hunk.id, \"accepted\")}\n                  >\n                    Accept\n                  </Button>\n                </div>\n              )}\n            </div>\n            <AnimatePresence initial={false}>\n              {!decision && (\n                <motion.div\n                  initial={{ height: 0 }}\n                  animate={{ height: \"auto\" }}\n                  exit={{ height: 0 }}\n                  transition={reduced ? { duration: 0 } : COLLAPSE}\n                  className=\"overflow-hidden\"\n                >\n                  <pre\n                    tabIndex={0}\n                    className=\"spotcheck-focus border-ink overflow-x-auto border-t-(length:--rule) py-1.5 font-(family-name:--f-mono) text-[12.5px] leading-[1.55]\"\n                  >\n                    {hunk.lines.map((line, index) => (\n                      <div key={index} className={cn(\"border-l-4 pr-3 pl-2\", LINE_STYLES[line.kind])}>\n                        <span aria-hidden>{PREFIXES[line.kind]} </span>\n                        <span className=\"sr-only\">{SPOKEN[line.kind]}</span>\n                        {line.text}\n                      </div>\n                    ))}\n                  </pre>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </section>\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:component"
}