{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reviewable-diff",
  "title": "Reviewable Diff",
  "description": "A proposed change staged a hunk at a time, so a good patch with one bad hunk is not all or nothing.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/diff-view.json"
  ],
  "files": [
    {
      "path": "registry/default/reviewable-diff/reviewable-diff.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport {\n  diffLines,\n  toHunks,\n  type DiffHunk,\n  type DiffLineKind,\n} from \"@/registry/default/diff-view/diff-view\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ReviewableDiffProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"onChange\"\n> & {\n  before?: string\n  after?: string\n  /** Already split into hunks, when you have them from a real diff. */\n  hunks?: readonly DiffHunk[]\n  filename?: string\n  context?: number\n  /** Hunk indexes that start staged. Defaults to all of them. */\n  defaultStaged?: readonly number[]\n  staged?: readonly number[]\n  onStagedChange?: (staged: number[]) => void\n  onApply?: (hunks: DiffHunk[]) => void\n  applyLabel?: string\n}\n\nconst tone: Record<DiffLineKind, string> = {\n  add: \"bg-[color-mix(in_oklab,var(--accent)_18%,transparent)]\",\n  remove: \"bg-[color-mix(in_oklab,var(--destructive)_14%,transparent)]\",\n  context: \"\",\n}\n\nconst sign: Record<DiffLineKind, string> = {\n  add: \"+\",\n  remove: \"-\",\n  context: \" \",\n}\n\nfunction countOf(hunk: DiffHunk) {\n  let added = 0\n  let removed = 0\n\n  for (const line of hunk.lines) {\n    if (line.kind === \"add\") added++\n    if (line.kind === \"remove\") removed++\n  }\n\n  return { added, removed }\n}\n\n/**\n * A proposed change reviewed a hunk at a time: take three of the seven, leave\n * the rest, apply what you took.\n *\n * All or nothing is the wrong shape for a change somebody else wrote. The\n * useful half of an agent's diff and the part that misunderstood the codebase\n * usually arrive in the same patch.\n */\nexport function ReviewableDiff({\n  before = \"\",\n  after = \"\",\n  hunks,\n  filename,\n  context = 3,\n  defaultStaged,\n  staged,\n  onStagedChange,\n  onApply,\n  applyLabel = \"Apply staged\",\n  className,\n  ...rootProps\n}: ReviewableDiffProps) {\n  const resolved = React.useMemo(\n    () => hunks ?? toHunks(diffLines(before, after), context),\n    [hunks, before, after, context]\n  )\n\n  // Staged indexes belong to one set of hunks. Keeping them beside the hunks\n  // they were chosen from means a new diff starts fresh rather than carrying\n  // positions that now point at something else.\n  const [held, setHeld] = React.useState<{\n    of: readonly DiffHunk[]\n    staged: readonly number[]\n  } | null>(null)\n\n  const chosen =\n    staged ??\n    (held?.of === resolved\n      ? held.staged\n      : (defaultStaged ?? resolved.map((_, at) => at)))\n  const isStaged = (at: number) => chosen.includes(at)\n\n  const set = (next: number[]) => {\n    if (staged === undefined) setHeld({ of: resolved, staged: next })\n    onStagedChange?.(next)\n  }\n\n  const toggle = (at: number) =>\n    set(\n      isStaged(at) ? chosen.filter((one) => one !== at) : [...chosen, at].sort()\n    )\n\n  const totals = chosen.reduce(\n    (sum, at) => {\n      const hunk = resolved[at]\n      if (!hunk) return sum\n      const { added, removed } = countOf(hunk)\n      return { added: sum.added + added, removed: sum.removed + removed }\n    },\n    { added: 0, removed: 0 }\n  )\n\n  const summary = `${chosen.length} of ${resolved.length} ${\n    resolved.length === 1 ? \"hunk\" : \"hunks\"\n  } staged, ${totals.added} added and ${totals.removed} removed`\n\n  return (\n    <div\n      data-slot=\"reviewable-diff\"\n      className={cn(\n        \"border-border bg-background w-full overflow-hidden rounded-xl border font-mono text-xs\",\n        className\n      )}\n      {...rootProps}\n    >\n      <div className=\"border-border flex items-center gap-3 border-b px-3 py-2\">\n        {filename ? (\n          <span className=\"text-foreground truncate font-medium\">\n            {filename}\n          </span>\n        ) : null}\n        <span className=\"text-muted-foreground ms-auto shrink-0 font-sans text-xs tabular-nums\">\n          {chosen.length}/{resolved.length} hunks\n          <span className=\"ms-2 text-[color-mix(in_oklab,var(--accent)_75%,var(--foreground))]\">\n            +{totals.added}\n          </span>\n          <span className=\"text-destructive ms-1.5\">-{totals.removed}</span>\n        </span>\n      </div>\n\n      {resolved.length === 0 ? (\n        <p className=\"text-muted-foreground px-3 py-4 font-sans text-xs\">\n          No changes.\n        </p>\n      ) : (\n        <>\n          <ul role=\"group\" aria-label={`Hunks in ${filename ?? \"the change\"}`}>\n            {resolved.map((hunk, at) => {\n              const { added, removed } = countOf(hunk)\n              const on = isStaged(at)\n              const title = hunk.header ?? `Hunk ${at + 1}`\n\n              return (\n                <li\n                  key={hunk.header ?? at}\n                  data-staged={on ? \"\" : undefined}\n                  className=\"border-border not-last:border-b\"\n                >\n                  <label\n                    className={cn(\n                      \"hover:bg-muted/50 focus-within:ring-ring flex cursor-pointer items-center gap-2 px-3 py-1.5 transition-colors focus-within:ring-2 focus-within:ring-inset motion-reduce:transition-none\",\n                      !on && \"opacity-60\"\n                    )}\n                  >\n                    <input\n                      type=\"checkbox\"\n                      checked={on}\n                      onChange={() => toggle(at)}\n                      /* Named here rather than left to the label, whose parts\n                         are joined without the spaces that sit between them. */\n                      aria-label={`Stage ${title}, ${added} added, ${removed} removed`}\n                      className=\"accent-primary size-4 shrink-0 focus-visible:outline-none\"\n                    />\n                    <span className=\"text-muted-foreground truncate font-sans\">\n                      {title}\n                    </span>\n                    <span className=\"text-muted-foreground ms-auto shrink-0 font-sans tabular-nums\">\n                      +{added} -{removed}\n                    </span>\n                  </label>\n\n                  <div className={cn(\"overflow-x-auto\", !on && \"opacity-45\")}>\n                    {hunk.lines.map((line, index) => (\n                      <div\n                        key={index}\n                        className={cn(\"flex whitespace-pre\", tone[line.kind])}\n                      >\n                        <span\n                          aria-hidden=\"true\"\n                          className=\"text-muted-foreground w-10 shrink-0 pe-2 text-end tabular-nums\"\n                        >\n                          {line.afterNumber ?? line.beforeNumber ?? \"\"}\n                        </span>\n                        <span className=\"text-muted-foreground w-4 shrink-0 select-none\">\n                          {sign[line.kind]}\n                        </span>\n                        <span className=\"pe-3\">{line.text}</span>\n                      </div>\n                    ))}\n                  </div>\n                </li>\n              )\n            })}\n          </ul>\n\n          <div className=\"border-border flex items-center gap-2 border-t px-3 py-2\">\n            <button\n              type=\"button\"\n              onClick={() => set(resolved.map((_, at) => at))}\n              className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-md px-2 py-1 font-sans text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n            >\n              Stage all\n            </button>\n            <button\n              type=\"button\"\n              onClick={() => set([])}\n              className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-md px-2 py-1 font-sans text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n            >\n              Stage none\n            </button>\n\n            <button\n              type=\"button\"\n              disabled={chosen.length === 0}\n              onClick={() =>\n                onApply?.(chosen.map((at) => resolved[at]!).filter(Boolean))\n              }\n              className=\"bg-foreground text-background focus-visible:ring-ring ms-auto inline-flex min-h-8 items-center rounded-md px-3 font-sans text-xs font-medium transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:outline-none disabled:opacity-40 motion-reduce:transition-none\"\n            >\n              {applyLabel}\n            </button>\n          </div>\n        </>\n      )}\n\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {summary}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:block"
    }
  ],
  "categories": [
    "ai",
    "agent",
    "code",
    "diff",
    "review"
  ],
  "type": "registry:block"
}