{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diff-table",
  "type": "registry:component",
  "title": "Diff table",
  "description": "AI-proposed edits sweeping through tabular data.",
  "categories": [
    "ai-interface"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/button.json"
  ],
  "files": [
    {
      "path": "src/registry/components/diff-table/diff-table.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"../button/button\";\n\n/* ─────────────────────────────────────────────────────────\n * DIFF TABLE\n * The proposed edit plays once and rests on the completed\n * diff. Each changed row is the control: click it to include\n * or exclude that specific addition/removal before applying.\n * ───────────────────────────────────────────────────────── */\n\nfunction useStage(steps: number[]) {\n  const [stage, setStage] = useState(0);\n  useEffect(() => {\n    if (stage >= steps.length) return undefined;\n    const t = setTimeout(() => setStage((s) => s + 1), steps[stage]);\n    return () => clearTimeout(t);\n  }, [stage, steps]);\n  return stage;\n}\n\nconst STAGE_DELAYS = [180, 260];\n\nexport type DiffRow = {\n  key: string;\n  id: string;\n  dept: string;\n  email: string;\n  removed: boolean;\n};\n\nconst ROWS: DiffRow[] = [\n  { key: \"pr-131\", id: \"pr-131\", dept: \"Preview\", email: \"nefeli\", removed: true },\n  { key: \"pr-142\", id: \"pr-142\", dept: \"Preview\", email: \"stelios\", removed: true },\n  { key: \"shop\", id: \"shop\", dept: \"Production\", email: \"katerina\", removed: false },\n];\n\n/* the row the proposed edit adds, shown below the table body once the diff settles */\nconst ADDED = { key: \"pr-157\", id: \"pr-157\", dept: \"Preview\", email: \"katerina\" };\n\nconst DOT: Record<string, string> = {\n  Production: \"bg-accent\",\n  Preview: \"bg-orange\",\n  Ephemeral: \"bg-ink-3\",\n};\n\nfunction IncludedMark({ included, tone }: { included: boolean; tone: \"red\" | \"green\" }) {\n  return (\n    <span\n      aria-hidden\n      className={`flex size-4.5 shrink-0 items-center justify-center rounded-[5px] transition-[background-color,color,transform] duration-150 ${\n        included\n          ? tone === \"red\"\n            ? \"bg-red text-white\"\n            : \"bg-green text-white\"\n          : \"bg-inset text-ink-3 shadow-hairline\"\n      }`}\n      style={{ transform: included ? \"scale(1)\" : \"scale(0.92)\" }}\n    >\n      {included ? (\n        <svg\n          width=\"11\"\n          height=\"11\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"3\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <path d=\"M20 6L9 17l-5-5\" />\n        </svg>\n      ) : null}\n    </span>\n  );\n}\n\nexport default function DiffTable({\n  rows = ROWS,\n}: {\n  rows?: DiffRow[];\n  variant?: string;\n} = {}) {\n  const stage = useStage(STAGE_DELAYS);\n  // 0 plain · 1 removals · 2 completed diff\n  const tinted = stage >= 1;\n  const settled = stage >= 2;\n  const [accepted, setAccepted] = useState(false);\n  // every proposed change starts included; each changed row toggles its own entry\n  const [edits, setEdits] = useState<Record<string, boolean>>(() => ({\n    ...Object.fromEntries(rows.filter((row) => row.removed).map((row) => [row.key, true])),\n    [ADDED.key]: true,\n  }));\n\n  const removals = rows.filter((row) => row.removed && edits[row.key]).length;\n  const additions = edits[ADDED.key] ? 1 : 0;\n  const showAdded = settled;\n\n  const toggleEdit = (key: string) => setEdits((current) => ({ ...current, [key]: !current[key] }));\n\n  return (\n    <div className=\"w-full max-w-95\">\n      <div className=\"relative overflow-hidden rounded-card bg-surface shadow-card\">\n        <div className=\"flex items-center justify-between border-b border-line px-3 py-2.5\">\n          <span className=\"text-[12.5px] font-medium text-ink\">Proposed preview cleanup</span>\n          {settled && !accepted && <span className=\"text-[11px] text-ink-3\">Click changed rows to toggle</span>}\n        </div>\n\n        <table className=\"w-full table-fixed border-collapse text-left\">\n          <colgroup>\n            <col className=\"w-[34%]\" />\n            <col className=\"w-[30%]\" />\n            <col className=\"w-[36%]\" />\n          </colgroup>\n          <thead>\n            <tr className=\"border-b border-line\">\n              {[\"Project\", \"Kind\", \"Owner\"].map((h) => (\n                <th key={h} className=\"px-3 py-2.5 text-[12px] font-medium text-ink-3\">\n                  {h}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {rows.map((row) => {\n              const out = row.removed && tinted && edits[row.key];\n              const interactive = row.removed && settled && !accepted;\n              return (\n                <tr\n                  key={row.key}\n                  tabIndex={interactive ? 0 : undefined}\n                  aria-selected={row.removed ? edits[row.key] : undefined}\n                  onClick={interactive ? () => toggleEdit(row.key) : undefined}\n                  onKeyDown={\n                    interactive\n                      ? (event) => {\n                          if (event.key === \"Enter\" || event.key === \" \") {\n                            event.preventDefault();\n                            toggleEdit(row.key);\n                          }\n                        }\n                      : undefined\n                  }\n                  className={`border-b border-line transition-[background-color,filter,opacity] duration-150 last:border-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent ${\n                    interactive ? \"cursor-pointer hover:brightness-[0.985]\" : \"\"\n                  }`}\n                  style={{ background: out ? \"var(--red-tint)\" : undefined }}\n                >\n                  <td\n                    className=\"px-3 py-2.5 text-[13px] font-medium tabular-nums transition-colors duration-200\"\n                    style={{ color: out ? \"var(--red)\" : \"var(--ink)\" }}\n                  >\n                    {row.id}\n                  </td>\n                  {/* oxlint-disable-next-line jsx-a11y/control-has-associated-label -- not a control; the pill's text sits deeper than the rule looks */}\n                  <td className=\"px-3 py-2.5\">\n                    <span\n                      className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-inset px-2 text-[11.5px] font-medium shadow-hairline transition-opacity duration-200\"\n                      style={{ opacity: out ? 0.55 : 1 }}\n                    >\n                      <span className={`size-1.5 rounded-full ${DOT[row.dept]}`} />\n                      <span className=\"text-ink-2\">{row.dept}</span>\n                    </span>\n                  </td>\n                  <td\n                    className=\"px-3 py-2.5 text-[12.5px] whitespace-nowrap transition-colors duration-200\"\n                    style={{\n                      color: out ? \"var(--red)\" : \"var(--ink-2)\",\n                      textDecorationLine: out ? \"line-through\" : \"none\",\n                      textDecorationColor: \"color-mix(in srgb, var(--red) 50%, transparent)\",\n                    }}\n                  >\n                    <span className=\"flex items-center justify-between gap-2\">\n                      <span className=\"min-w-0 truncate\">{row.email}</span>\n                      {row.removed && settled && <IncludedMark included={edits[row.key]} tone=\"red\" />}\n                    </span>\n                  </td>\n                </tr>\n              );\n            })}\n            {/* added row */}\n            <tr>\n              {/* oxlint-disable-next-line jsx-a11y/control-has-associated-label -- not a control; the row toggle inside carries its own aria-label */}\n              <td colSpan={3} className=\"p-0\">\n                <div\n                  className=\"grid transition-[grid-template-rows,opacity] duration-200\"\n                  style={{\n                    gridTemplateRows: showAdded ? \"1fr\" : \"0fr\",\n                    opacity: showAdded ? 1 : 0,\n                    transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n                  }}\n                >\n                  <div className=\"overflow-hidden\">\n                    <div\n                      role=\"checkbox\"\n                      tabIndex={accepted ? -1 : 0}\n                      aria-checked={edits[ADDED.key]}\n                      aria-label={`Include adding ${ADDED.id}`}\n                      onClick={accepted ? undefined : () => toggleEdit(ADDED.key)}\n                      onKeyDown={\n                        accepted\n                          ? undefined\n                          : (event) => {\n                              if (event.key === \"Enter\" || event.key === \" \") {\n                                event.preventDefault();\n                                toggleEdit(ADDED.key);\n                              }\n                            }\n                      }\n                      className={`grid grid-cols-[34%_30%_36%] items-center border-t border-line transition-[background-color,filter,opacity] duration-150 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent ${\n                        accepted ? \"\" : \"cursor-pointer hover:brightness-[0.985]\"\n                      }`}\n                      style={{ background: edits[ADDED.key] ? \"var(--green-tint)\" : undefined }}\n                    >\n                      <span\n                        className=\"px-3 py-2.5 text-[13px] font-medium tabular-nums transition-colors duration-200\"\n                        style={{ color: edits[ADDED.key] ? \"var(--green)\" : \"var(--ink-3)\" }}\n                      >\n                        {ADDED.id}\n                      </span>\n                      <span className=\"px-3 py-2.5\">\n                        <span className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-surface px-2 text-[11.5px] font-medium shadow-hairline\">\n                          <span className=\"size-1.5 rounded-full bg-green\" />\n                          <span className=\"text-ink-2\">{ADDED.dept}</span>\n                        </span>\n                      </span>\n                      <span\n                        className=\"px-3 py-2.5 text-[13px] transition-colors duration-200\"\n                        style={{ color: edits[ADDED.key] ? \"var(--green)\" : \"var(--ink-3)\" }}\n                      >\n                        <span className=\"flex items-center justify-between gap-2\">\n                          <span className=\"min-w-0 truncate\">{ADDED.email}</span>\n                          <IncludedMark included={edits[ADDED.key]} tone=\"green\" />\n                        </span>\n                      </span>\n                    </div>\n                  </div>\n                </div>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n\n        {/* footer — the summary follows the row-level selection */}\n        {settled && (\n          <div\n            className=\"flex min-h-11 items-center justify-between border-t border-line p-2.5\"\n            style={{ animation: \"fade-up 180ms cubic-bezier(0.23,1,0.32,1) both\" }}\n          >\n            {accepted ? (\n              <span\n                className=\"inline-flex items-center gap-1.5 rounded-full bg-green-tint py-1 pr-2.5 pl-1 text-[12.5px] font-medium text-green\"\n                style={{ animation: \"pop-in 180ms cubic-bezier(0.23,1,0.32,1) both\" }}\n              >\n                <span className=\"flex size-4.5 items-center justify-center rounded-full bg-green text-white\">\n                  <svg\n                    width=\"11\"\n                    height=\"11\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"3\"\n                    strokeLinecap=\"round\"\n                    strokeLinejoin=\"round\"\n                  >\n                    <path d=\"M20 6L9 17l-5-5\" />\n                  </svg>\n                </span>\n                {removals + additions} {removals + additions === 1 ? \"edit\" : \"edits\"} applied\n              </span>\n            ) : (\n              <>\n                <span className=\"text-[11.5px] tabular-nums text-ink-3\">\n                  {removals} {removals === 1 ? \"removal\" : \"removals\"} · {additions}{\" \"}\n                  {additions === 1 ? \"addition\" : \"additions\"}\n                </span>\n                <span className=\"flex items-center gap-1.5\">\n                  <Button\n                    variant=\"accent\"\n                    size=\"sm\"\n                    disabled={removals + additions === 0}\n                    onClick={() => {\n                      setAccepted(true);\n                    }}\n                    className=\"text-[12px]\"\n                  >\n                    Apply {removals + additions} {removals + additions === 1 ? \"change\" : \"changes\"}\n                  </Button>\n                </span>\n              </>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/diff-table/diff-table.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/diff-table",
  "meta": {
    "kind": "component",
    "group": "AI interface",
    "export": "DiffTable",
    "docs": "https://irisui.dev/components/diff-table"
  }
}
