{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-table",
  "title": "Diff table",
  "description": "AI-proposed table changes with staged reveal and row-level or bulk accept/reject decisions.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/DiffTable.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport { useReducedMotion } from \"./useReducedMotion\";\n\nexport interface DiffTableColumn<T> { key: string; header: ReactNode; render?: (row: T) => ReactNode; }\nexport interface DiffTableRow { id: string; cells: readonly ReactNode[]; change?: \"added\" | \"removed\" | \"unchanged\"; }\nexport interface DiffTableProps {\n  title?: ReactNode;\n  columns: readonly DiffTableColumn<DiffTableRow>[];\n  rows: readonly DiffTableRow[];\n  onRowDecision?: (row: DiffTableRow, decision: \"accept\" | \"reject\") => void;\n  onBulkDecision?: (decision: \"accept\" | \"reject\") => void;\n  animate?: boolean;\n  className?: string;\n}\n\nexport function DiffTable({ title, columns, rows, onRowDecision, onBulkDecision, animate = true, className = \"\" }: DiffTableProps) {\n  const reducedMotion = useReducedMotion();\n  const [stage, setStage] = useState(animate ? 0 : 2);\n  const [decisions, setDecisions] = useState<Record<string, \"accept\" | \"reject\">>({});\n  useEffect(() => {\n    if (!animate || reducedMotion) {\n      setStage(2);\n      return;\n    }\n    if (stage >= 2) return;\n    const timer = window.setTimeout(() => setStage((value) => value + 1), stage === 0 ? 800 : 1000);\n    return () => window.clearTimeout(timer);\n  }, [animate, reducedMotion, stage]);\n  const visible = stage >= 1;\n  const completed = stage >= 2;\n  const decide = (row: DiffTableRow, decision: \"accept\" | \"reject\") => {\n    setDecisions((current) => ({ ...current, [row.id]: decision }));\n    onRowDecision?.(row, decision);\n  };\n  const decideAll = (decision: \"accept\" | \"reject\") => {\n    setDecisions(Object.fromEntries(rows.map((row) => [row.id, decision])));\n    onBulkDecision?.(decision);\n  };\n  return <div data-ai-table=\"diff\" className={`w-full max-w-2xl ${className}`}>\n    <div className=\"overflow-hidden rounded-xl border bg-card shadow-sm\">\n      {title || onBulkDecision ? <div className=\"flex items-center justify-between border-b border-border px-4 py-3\"><span className=\"text-sm font-medium\">{title}</span>{onBulkDecision ? <span className=\"flex gap-1\"><button type=\"button\" onClick={() => decideAll(\"accept\")} className=\"rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-muted\">Accept all</button><button type=\"button\" onClick={() => decideAll(\"reject\")} className=\"rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-muted\">Reject all</button></span> : null}</div> : null}\n      <div className=\"overflow-x-auto\" tabIndex={0} aria-label=\"Scrollable proposed changes table\"><table className=\"w-full min-w-[34rem] border-collapse text-left text-sm\"><thead><tr className=\"border-b border-border\">{columns.map((column) => <th key={column.key} scope=\"col\" className=\"px-4 py-2.5 text-xs font-medium text-muted-foreground\">{column.header}</th>)}{onRowDecision ? <th scope=\"col\" className=\"px-4 py-2.5 text-right text-xs font-medium text-muted-foreground\">Decision</th> : null}</tr></thead>\n        <tbody>{rows.map((row) => { const changed = completed && row.change && row.change !== \"unchanged\"; const hidden = row.change === \"added\" && !visible; const decision = decisions[row.id]; return <tr key={row.id} className={`border-b border-border last:border-0 transition-colors hover:bg-muted ${hidden ? \"hidden\" : \"\"} ${changed && row.change === \"removed\" ? \"bg-destructive/10\" : \"\"} ${changed && row.change === \"added\" ? \"bg-success/10\" : \"\"}`}>\n          {columns.map((column, index) => <td key={column.key} className={`px-4 py-3 ${changed && row.change === \"removed\" ? \"text-destructive line-through\" : changed && row.change === \"added\" ? \"text-success\" : index === 0 ? \"font-medium\" : \"text-muted-foreground\"}`}>{column.render ? column.render(row) : row.cells[index]}</td>)}\n          {onRowDecision ? <td className=\"px-4 py-3 text-right\"><span className=\"inline-flex gap-1\">{([\"accept\", \"reject\"] as const).map((item) => <button key={item} type=\"button\" aria-label={`${item} ${row.id}`} aria-pressed={decision === item} onClick={() => decide(row, item)} className={`rounded-md px-2 py-1 text-xs transition-colors motion-reduce:transition-none ${decision === item ? item === \"accept\" ? \"bg-success text-background\" : \"bg-destructive text-background\" : \"text-muted-foreground hover:bg-muted\"}`}>{item === \"accept\" ? \"Accept\" : \"Reject\"}</button>)}</span></td> : null}\n        </tr>; })}</tbody></table></div>\n    </div>\n  </div>;\n}\n\nexport default DiffTable;\n",
      "type": "registry:component",
      "target": "@components/ai/DiffTable.tsx"
    }
  ],
  "type": "registry:component"
}
