{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "editable-table",
  "title": "Editable Table",
  "description": "Inline cell editing with per-cell validation, optimistic updates against a simulated save, and single-step undo.",
  "dependencies": [
    "@tanstack/react-table"
  ],
  "registryDependencies": [
    "table",
    "button",
    "input"
  ],
  "files": [
    {
      "path": "src/components/editable/columns.tsx",
      "content": "export type Product = {\n  id: string\n  name: string\n  price: number\n  stock: number\n  category: string\n}\n\nexport type FieldType = 'text' | 'number'\n\nexport interface FieldConfig {\n  id: 'name' | 'price' | 'stock'\n  label: string\n  type: FieldType\n  validate: (raw: string) => string | null\n}\n\nexport const editableFields: FieldConfig[] = [\n  {\n    id: 'name',\n    label: 'Name',\n    type: 'text',\n    validate: (raw) => (raw.trim().length > 0 ? null : 'Name is required'),\n  },\n  {\n    id: 'price',\n    label: 'Price',\n    type: 'number',\n    validate: (raw) => {\n      const n = Number(raw)\n      if (raw.trim() === '' || Number.isNaN(n)) return 'Enter a number'\n      if (n < 0) return 'Must be 0 or more'\n      return null\n    },\n  },\n  {\n    id: 'stock',\n    label: 'Stock',\n    type: 'number',\n    validate: (raw) => {\n      const n = Number(raw)\n      if (raw.trim() === '' || Number.isNaN(n) || !Number.isInteger(n)) {\n        return 'Enter a whole number'\n      }\n      if (n < 0) return 'Must be 0 or more'\n      return null\n    },\n  },\n]\n",
      "type": "registry:component",
      "target": "@components/tables/editable-table/columns.tsx"
    },
    {
      "path": "src/components/editable/data-table.tsx",
      "content": "'use client'\n\nimport { useMemo, useRef, useState } from 'react'\nimport {\n  flexRender,\n  getCoreRowModel,\n  useReactTable,\n} from '@tanstack/react-table'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Undo2 } from 'lucide-react'\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '#/components/ui/table'\nimport { Button } from '#/components/ui/button'\nimport { Input } from '#/components/ui/input'\nimport { cn } from '#/lib/utils.ts'\nimport type { FieldConfig, Product } from './columns'\nimport { editableFields } from './columns'\n\ninterface HistoryEntry {\n  rowId: string\n  columnId: string\n  previousValue: string | number\n}\n\nconst currency = (value: number) =>\n  value.toLocaleString('en-US', { style: 'currency', currency: 'USD' })\n\nfunction cellKey(rowId: string, columnId: string) {\n  return `${rowId}:${columnId}`\n}\n\ninterface EditableTableProps {\n  data: Product[]\n  onDataChange: (updater: (prev: Product[]) => Product[]) => void\n}\n\nexport function EditableTable({ data, onDataChange }: EditableTableProps) {\n  const [editingCell, setEditingCell] = useState<{\n    rowId: string\n    columnId: string\n  } | null>(null)\n  const [draftValue, setDraftValue] = useState('')\n  const [errors, setErrors] = useState<Record<string, string>>({})\n  const [pendingCells, setPendingCells] = useState<Set<string>>(new Set())\n  const [history, setHistory] = useState<HistoryEntry[]>([])\n  const errorTimers = useRef<Record<string, ReturnType<typeof setTimeout>>>({})\n\n  function startEdit(rowId: string, columnId: string, currentValue: string | number) {\n    setEditingCell({ rowId, columnId })\n    setDraftValue(String(currentValue))\n  }\n\n  function cancelEdit() {\n    setEditingCell(null)\n  }\n\n  function flashError(key: string, message: string) {\n    setErrors((prev) => ({ ...prev, [key]: message }))\n    clearTimeout(errorTimers.current[key])\n    errorTimers.current[key] = setTimeout(() => {\n      setErrors((prev) => {\n        const next = { ...prev }\n        delete next[key]\n        return next\n      })\n    }, 2000)\n  }\n\n  function saveOptimistically(\n    rowId: string,\n    columnId: string,\n    previousValue: string | number,\n    key: string,\n  ) {\n    setPendingCells((prev) => new Set(prev).add(key))\n    setTimeout(() => {\n      const failed = Math.random() < 0.15\n      setPendingCells((prev) => {\n        const next = new Set(prev)\n        next.delete(key)\n        return next\n      })\n      if (failed) {\n        onDataChange((prev) =>\n          prev.map((r) =>\n            r.id === rowId ? { ...r, [columnId]: previousValue } : r,\n          ),\n        )\n        setHistory((prev) => {\n          const fromEnd = [...prev]\n            .reverse()\n            .findIndex((h) => h.rowId === rowId && h.columnId === columnId)\n          if (fromEnd === -1) return prev\n          const index = prev.length - 1 - fromEnd\n          return prev.filter((_, i) => i !== index)\n        })\n        flashError(key, 'Save failed — reverted')\n      }\n    }, 600)\n  }\n\n  function commitEdit(field: FieldConfig) {\n    if (!editingCell) return\n    const { rowId, columnId } = editingCell\n    const key = cellKey(rowId, columnId)\n    const error = field.validate(draftValue)\n\n    if (error) {\n      flashError(key, error)\n      setEditingCell(null)\n      return\n    }\n\n    const parsedValue = field.type === 'number' ? Number(draftValue) : draftValue\n    const row = data.find((r) => r.id === rowId)\n    setEditingCell(null)\n    if (!row) return\n\n    const previousValue = row[columnId as keyof Product] as string | number\n    if (previousValue === parsedValue) return\n\n    onDataChange((prev) =>\n      prev.map((r) => (r.id === rowId ? { ...r, [columnId]: parsedValue } : r)),\n    )\n    setHistory((prev) => [...prev, { rowId, columnId, previousValue }])\n    saveOptimistically(rowId, columnId, previousValue, key)\n  }\n\n  function undo() {\n    setHistory((prev) => {\n      if (prev.length === 0) return prev\n      const last = prev[prev.length - 1]\n      onDataChange((data) =>\n        data.map((r) =>\n          r.id === last.rowId ? { ...r, [last.columnId]: last.previousValue } : r,\n        ),\n      )\n      return prev.slice(0, -1)\n    })\n  }\n\n  const columns = useMemo<ColumnDef<Product>[]>(() => {\n    const editable: ColumnDef<Product>[] = editableFields.map((field) => ({\n      id: field.id,\n      header: field.label,\n      accessorKey: field.id,\n      cell: ({ row, getValue }) => {\n        const key = cellKey(row.original.id, field.id)\n        const isEditing =\n          editingCell?.rowId === row.original.id &&\n          editingCell.columnId === field.id\n        const isPending = pendingCells.has(key)\n        const error = errors[key]\n        const value = getValue() as string | number\n\n        if (isEditing) {\n          return (\n            <Input\n              autoFocus\n              value={draftValue}\n              type={field.type === 'number' ? 'number' : 'text'}\n              onChange={(e) => setDraftValue(e.target.value)}\n              onBlur={() => commitEdit(field)}\n              onKeyDown={(e) => {\n                if (e.key === 'Enter') commitEdit(field)\n                if (e.key === 'Escape') cancelEdit()\n              }}\n              className=\"h-7 px-2\"\n            />\n          )\n        }\n\n        return (\n          <button\n            type=\"button\"\n            onClick={() => startEdit(row.original.id, field.id, value)}\n            className={cn(\n              'w-full rounded px-1.5 py-1 text-left hover:bg-muted',\n              isPending && 'opacity-50',\n              error && 'text-destructive',\n            )}\n          >\n            {error ?? (field.id === 'price' ? currency(value as number) : value)}\n          </button>\n        )\n      },\n    }))\n\n    return [\n      { accessorKey: 'id', header: 'SKU' },\n      ...editable,\n      { accessorKey: 'category', header: 'Category' },\n    ]\n  }, [editingCell, draftValue, errors, pendingCells])\n\n  const table = useReactTable({\n    data,\n    columns,\n    getCoreRowModel: getCoreRowModel(),\n  })\n\n  return (\n    <div>\n      <div className=\"mb-3 flex flex-wrap items-center justify-between gap-2\">\n        <p className=\"text-sm text-muted-foreground\">\n          Click a Name, Price, or Stock cell to edit it.\n        </p>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"sm\"\n          onClick={undo}\n          disabled={history.length === 0}\n        >\n          <Undo2 className=\"h-3.5 w-3.5\" />\n          Undo{history.length > 0 ? ` (${history.length})` : ''}\n        </Button>\n      </div>\n      <div className=\"overflow-hidden rounded-md border\">\n        <Table>\n          <TableHeader>\n            {table.getHeaderGroups().map((headerGroup) => (\n              <TableRow key={headerGroup.id}>\n                {headerGroup.headers.map((header) => (\n                  <TableHead key={header.id}>\n                    {header.isPlaceholder\n                      ? null\n                      : flexRender(\n                          header.column.columnDef.header,\n                          header.getContext(),\n                        )}\n                  </TableHead>\n                ))}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {table.getRowModel().rows.map((row) => (\n              <TableRow key={row.id}>\n                {row.getVisibleCells().map((cell) => (\n                  <TableCell key={cell.id}>\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/tables/editable-table/data-table.tsx"
    },
    {
      "path": "src/components/editable/index.tsx",
      "content": "import { useState } from 'react'\n\nimport { EditableTable } from './data-table'\nimport type { Product } from './columns'\n\nconst initialData: Product[] = [\n  { id: 'SKU-001', name: 'Wireless Mouse', price: 29.99, stock: 142, category: 'Electronics' },\n  { id: 'SKU-002', name: 'Mechanical Keyboard', price: 89.0, stock: 58, category: 'Electronics' },\n  { id: 'SKU-003', name: 'Desk Lamp', price: 34.5, stock: 76, category: 'Home' },\n  { id: 'SKU-004', name: 'Canvas Tote', price: 18.0, stock: 203, category: 'Clothing' },\n  { id: 'SKU-005', name: 'Ceramic Mug', price: 12.99, stock: 310, category: 'Home' },\n]\n\nexport function EditableTableDemo() {\n  const [data, setData] = useState(initialData)\n\n  return <EditableTable data={data} onDataChange={setData} />\n}\n",
      "type": "registry:component",
      "target": "@components/tables/editable-table/index.tsx"
    }
  ],
  "type": "registry:block"
}