{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"diff-table","title":"Diff Table","description":"AI-proposed edits sweeping through tabular data.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json","https://ward.so/r/button.json"],"files":[{"path":"registry/ward/DiffTable.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"@/registry/ward/ui/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;\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: \"rocky\", id: \"Rocky Road\", dept: \"Classic\", email: \"aurora-works\", removed: true },\n  { key: \"bubblegum\", id: \"Bubblegum\", dept: \"Retro\", email: \"kumo-studio\", removed: true },\n  { key: \"mint\", id: \"Mint Chip\", dept: \"Classic\", email: \"maple-orbit\", removed: false },\n];\n\nconst DOT: Record<string, string> = {\n  Classic: \"bg-highlight\",\n  Retro: \"bg-ink-3\",\n  Seasonal: \"bg-orange\",\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\" ? \"bg-red text-white\" : \"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 width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></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  const [edits, setEdits] = useState<Record<string, boolean>>({ rocky: true, bubblegum: true, pistachio: true });\n\n  const removals = [\"rocky\", \"bubblegum\"].filter((key) => edits[key]).length;\n  const additions = edits.pistachio ? 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-card shadow-card\">\n        <div className=\"primitive-card-bar flex items-center justify-between border-b border-line\">\n          <span className=\"text-[12.5px] font-medium text-ink\">Proposed menu 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\", \"Category\", \"Supplier\"].map((h) => (\n                <th key={h} className=\"primitive-table-cell 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={interactive ? (event) => {\n                    if (event.key === \"Enter\" || event.key === \" \") {\n                      event.preventDefault();\n                      toggleEdit(row.key);\n                    }\n                  } : undefined}\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-highlight ${\n                    interactive ? \"cursor-pointer hover:brightness-[0.985]\" : \"\"\n                  }`}\n                  style={{ background: out ? \"var(--red-tint)\" : undefined }}\n                >\n                  <td\n                    className=\"primitive-table-cell 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                  <td className=\"primitive-table-cell\">\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=\"primitive-table-cell 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              <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.pistachio}\n                      aria-label=\"Include adding Pistachio\"\n                      onClick={accepted ? undefined : () => toggleEdit(\"pistachio\")}\n                      onKeyDown={accepted ? undefined : (event) => {\n                        if (event.key === \"Enter\" || event.key === \" \") {\n                          event.preventDefault();\n                          toggleEdit(\"pistachio\");\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-highlight ${\n                        accepted ? \"\" : \"cursor-pointer hover:brightness-[0.985]\"\n                      }`}\n                      style={{ background: edits.pistachio ? \"var(--green-tint)\" : undefined }}\n                    >\n                      <span className=\"primitive-table-cell text-[13px] font-medium tabular-nums transition-colors duration-200\" style={{ color: edits.pistachio ? \"var(--green)\" : \"var(--ink-3)\" }}>\n                        Pistachio\n                      </span>\n                      <span className=\"primitive-table-cell\">\n                        <span className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-card 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\">Seasonal</span>\n                        </span>\n                      </span>\n                      <span className=\"primitive-table-cell text-[13px] transition-colors duration-200\" style={{ color: edits.pistachio ? \"var(--green)\" : \"var(--ink-3)\" }}>\n                        <span className=\"flex items-center justify-between gap-2\">\n                          <span className=\"min-w-0 truncate\">maple-orbit</span>\n                          <IncludedMark included={edits.pistachio} 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=\"primitive-card-footer flex min-h-11 items-center justify-between border-t border-line\"\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 width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></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} {additions === 1 ? \"addition\" : \"additions\"}\n                </span>\n                <span className=\"flex items-center gap-1.5\">\n                  <Button\n                    variant=\"default\"\n                    size=\"sm\"\n                    disabled={removals + additions === 0}\n                    onClick={() => {\n                      setAccepted(true);\n                    }}\n                    className=\"h-7 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/ward/DiffTable.tsx"}],"meta":{"variants":["Default"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/DiffTable.tsx","access":"free"},"categories":["data"],"type":"registry:component"}