{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "A fully-featured, animated data table with sorting, search, selection, inline editing, column resizing, and pagination.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/wise-ui/components/data-table.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, AnimatePresence } from \"motion/react\"\nimport { cn } from \"@/registry/wise-ui/lib/utils\"\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport interface DataTableColumn<T> {\n  key: keyof T & string\n  label: string\n  sortable?: boolean\n  resizable?: boolean\n  editable?: boolean\n  width?: number\n  render?: (value: T[keyof T], row: T) => React.ReactNode\n}\n\nexport interface DataTableProps<T extends { id: string | number }> {\n  data: T[]\n  columns: DataTableColumn<T>[]\n  searchable?: boolean\n  searchKeys?: (keyof T)[]\n  selectable?: boolean\n  onSelectionChange?: (rows: T[]) => void\n  paginated?: boolean\n  pageSize?: number\n  className?: string\n}\n\ntype SortDir = \"asc\" | \"desc\" | null\n\n// ── Helpers ────────────────────────────────────────────────────────────────────\n\nfunction useDebounce<T>(value: T, delay: number): T {\n  const [debounced, setDebounced] = React.useState(value)\n  React.useEffect(() => {\n    const t = setTimeout(() => setDebounced(value), delay)\n    return () => clearTimeout(t)\n  }, [value, delay])\n  return debounced\n}\n\n// ── Sub-components ─────────────────────────────────────────────────────────────\n\nfunction DataTableToolbar({\n  searchValue,\n  onSearchChange,\n  totalRows,\n  filteredRows,\n}: {\n  searchValue: string\n  onSearchChange: (v: string) => void\n  totalRows: number\n  filteredRows: number\n}) {\n  return (\n    <div className=\"flex items-center justify-between gap-4 pb-4\">\n      <div className=\"relative\">\n        <svg\n          className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <circle cx=\"11\" cy=\"11\" r=\"8\" />\n          <path d=\"m21 21-4.3-4.3\" />\n        </svg>\n        <input\n          type=\"text\"\n          placeholder=\"Search...\"\n          value={searchValue}\n          onChange={(e) => onSearchChange(e.target.value)}\n          className=\"h-9 w-64 rounded-md border border-border bg-background pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring/40\"\n        />\n      </div>\n      <span className=\"text-xs text-muted-foreground\">\n        {filteredRows === totalRows\n          ? `${totalRows} rows`\n          : `${filteredRows} of ${totalRows} rows`}\n      </span>\n    </div>\n  )\n}\n\nfunction DataTableHeader<T extends { id: string | number }>({\n  columns,\n  colWidths,\n  sortKey,\n  sortDir,\n  onSort,\n  onResizeStart,\n  selectable,\n  allSelected,\n  onToggleAll,\n}: {\n  columns: DataTableColumn<T>[]\n  colWidths: number[]\n  sortKey: string | null\n  sortDir: SortDir\n  onSort: (key: string) => void\n  onResizeStart: (colIdx: number, startX: number) => void\n  selectable?: boolean\n  allSelected: boolean\n  onToggleAll: () => void\n}) {\n  return (\n    <div\n      className=\"grid border-b border-border\"\n      style={{\n        gridTemplateColumns: [\n          ...(selectable ? [\"40px\"] : []),\n          ...colWidths.map((w) => `${w}px`),\n        ].join(\" \"),\n      }}\n    >\n      {selectable && (\n        <div className=\"flex items-center justify-center py-3\">\n          <RoundCheckbox checked={allSelected} onChange={onToggleAll} />\n        </div>\n      )}\n      {columns.map((col, i) => (\n        <div\n          key={col.key}\n          className=\"group relative flex select-none items-center gap-1.5 px-4 py-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground\"\n        >\n          <span\n            className={cn(\n              col.sortable && \"cursor-pointer hover:text-foreground transition-colors\"\n            )}\n            onClick={() => col.sortable && onSort(col.key)}\n          >\n            {col.label}\n          </span>\n          {col.sortable && sortKey === col.key && sortDir && (\n            <svg\n              width=\"12\"\n              height=\"12\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2.5\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              className={cn(\n                \"shrink-0 text-primary transition-transform\",\n                sortDir === \"desc\" && \"rotate-180\"\n              )}\n            >\n              <path d=\"m18 15-6-6-6 6\" />\n            </svg>\n          )}\n          {(col.resizable !== false) && i < columns.length - 1 && (\n            <div\n              className=\"absolute right-0 top-1/2 z-10 h-4 w-1 -translate-y-1/2 cursor-col-resize rounded-full bg-border opacity-0 transition-opacity group-hover:opacity-100\"\n              onMouseDown={(e) => {\n                e.preventDefault()\n                onResizeStart(i, e.clientX)\n              }}\n            />\n          )}\n        </div>\n      ))}\n    </div>\n  )\n}\n\nfunction DataTableBody<T extends { id: string | number }>({\n  rows,\n  columns,\n  colWidths,\n  selectable,\n  selectedIds,\n  onToggleRow,\n  editingCell,\n  onStartEdit,\n  onSaveEdit,\n  onCancelEdit,\n}: {\n  rows: T[]\n  columns: DataTableColumn<T>[]\n  colWidths: number[]\n  selectable?: boolean\n  selectedIds: Set<string | number>\n  onToggleRow: (id: string | number) => void\n  editingCell: { rowId: string | number; colKey: string } | null\n  onStartEdit: (rowId: string | number, colKey: string) => void\n  onSaveEdit: (rowId: string | number, colKey: string, value: string) => void\n  onCancelEdit: () => void\n}) {\n  return (\n    <AnimatePresence mode=\"popLayout\">\n      {rows.map((row) => (\n        <motion.div\n          key={row.id}\n          layout\n          initial={{ opacity: 0, y: -8 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: 8 }}\n          transition={{ duration: 0.2 }}\n          className={cn(\n            \"grid border-b border-border transition-colors\",\n            selectedIds.has(row.id) && \"bg-primary/5\"\n          )}\n          style={{\n            gridTemplateColumns: [\n              ...(selectable ? [\"40px\"] : []),\n              ...colWidths.map((w) => `${w}px`),\n            ].join(\" \"),\n          }}\n        >\n          {selectable && (\n            <div className=\"flex items-center justify-center py-3\">\n              <RoundCheckbox checked={selectedIds.has(row.id)} onChange={() => onToggleRow(row.id)} />\n            </div>\n          )}\n          {columns.map((col) => {\n            const value = row[col.key]\n            const isEditing =\n              editingCell?.rowId === row.id && editingCell?.colKey === col.key\n\n            return (\n              <div\n                key={col.key}\n                className=\"flex items-center px-4 py-3 text-sm text-foreground\"\n                onDoubleClick={() => {\n                  if (col.editable) onStartEdit(row.id, col.key)\n                }}\n              >\n                {isEditing ? (\n                  <EditableCell\n                    value={String(value ?? \"\")}\n                    onSave={(v) => onSaveEdit(row.id, col.key, v)}\n                    onCancel={onCancelEdit}\n                  />\n                ) : col.render ? (\n                  col.render(value, row)\n                ) : (\n                  <span className=\"truncate\">{String(value ?? \"\")}</span>\n                )}\n              </div>\n            )\n          })}\n        </motion.div>\n      ))}\n    </AnimatePresence>\n  )\n}\n\nfunction EditableCell({\n  value,\n  onSave,\n  onCancel,\n}: {\n  value: string\n  onSave: (v: string) => void\n  onCancel: () => void\n}) {\n  const [editValue, setEditValue] = React.useState(value)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n\n  React.useEffect(() => {\n    inputRef.current?.focus()\n    inputRef.current?.select()\n  }, [])\n\n  return (\n    <input\n      ref={inputRef}\n      type=\"text\"\n      value={editValue}\n      onChange={(e) => setEditValue(e.target.value)}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\") onSave(editValue)\n        if (e.key === \"Escape\") onCancel()\n      }}\n      onBlur={() => onSave(editValue)}\n      className=\"h-7 w-full rounded border border-primary/50 bg-background px-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary/40\"\n    />\n  )\n}\n\nfunction DataTableBatchBar({\n  count,\n  onClear,\n}: {\n  count: number\n  onClear: () => void\n}) {\n  return (\n    <AnimatePresence>\n      {count > 0 && (\n        <motion.div\n          initial={{ opacity: 0, y: 16 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: 16 }}\n          transition={{ duration: 0.2 }}\n          className=\"mt-3 flex items-center justify-between rounded-lg border border-border bg-card px-4 py-2.5\"\n        >\n          <span className=\"text-sm text-foreground\">\n            <strong>{count}</strong> row{count !== 1 && \"s\"} selected\n          </span>\n          <button\n            onClick={onClear}\n            className=\"rounded-md px-3 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground\"\n          >\n            Clear selection\n          </button>\n        </motion.div>\n      )}\n    </AnimatePresence>\n  )\n}\n\nfunction DataTablePagination({\n  page,\n  totalPages,\n  pageSize,\n  totalRows,\n  onPageChange,\n}: {\n  page: number\n  totalPages: number\n  pageSize: number\n  totalRows: number\n  onPageChange: (p: number) => void\n}) {\n  const start = (page - 1) * pageSize + 1\n  const end = Math.min(page * pageSize, totalRows)\n\n  return (\n    <div className=\"flex items-center justify-between pt-4\">\n      <span className=\"text-xs text-muted-foreground\">\n        Showing {start}–{end} of {totalRows}\n      </span>\n      <div className=\"flex items-center gap-1\">\n        <button\n          disabled={page <= 1}\n          onClick={() => onPageChange(page - 1)}\n          className=\"rounded-md border border-border px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40\"\n        >\n          Prev\n        </button>\n        {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (\n          <button\n            key={p}\n            onClick={() => onPageChange(p)}\n            className={cn(\n              \"rounded-md px-2.5 py-1 text-xs font-medium transition-colors\",\n              p === page\n                ? \"bg-primary text-primary-foreground\"\n                : \"text-muted-foreground hover:bg-accent hover:text-foreground\"\n            )}\n          >\n            {p}\n          </button>\n        ))}\n        <button\n          disabled={page >= totalPages}\n          onClick={() => onPageChange(page + 1)}\n          className=\"rounded-md border border-border px-2.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40\"\n        >\n          Next\n        </button>\n      </div>\n    </div>\n  )\n}\n\nfunction RoundCheckbox({\n  checked,\n  onChange,\n}: {\n  checked: boolean\n  onChange: () => void\n}) {\n  return (\n    <button\n      type=\"button\"\n      role=\"checkbox\"\n      aria-checked={checked}\n      onClick={onChange}\n      className={cn(\n        \"flex size-[18px] shrink-0 items-center justify-center rounded-full border-2 transition-all duration-150\",\n        checked\n          ? \"border-primary bg-primary text-primary-foreground\"\n          : \"border-border bg-background hover:border-muted-foreground/50\"\n      )}\n    >\n      {checked && (\n        <svg\n          width=\"10\"\n          height=\"10\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"3.5\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        >\n          <polyline points=\"20 6 9 17 4 12\" />\n        </svg>\n      )}\n    </button>\n  )\n}\n\n// ── Main Component ─────────────────────────────────────────────────────────────\n\nexport function DataTable<T extends { id: string | number }>({\n  data: initialData,\n  columns,\n  searchable = false,\n  searchKeys,\n  selectable = false,\n  onSelectionChange,\n  paginated = false,\n  pageSize = 10,\n  className,\n}: DataTableProps<T>) {\n  // ── Data state (for inline editing) ────────────────────────────────────────\n  const [data, setData] = React.useState(initialData)\n  React.useEffect(() => setData(initialData), [initialData])\n\n  // ── Search ─────────────────────────────────────────────────────────────────\n  const [searchValue, setSearchValue] = React.useState(\"\")\n  const debouncedSearch = useDebounce(searchValue, 200)\n\n  const filteredData = React.useMemo(() => {\n    if (!debouncedSearch) return data\n    const keys = searchKeys ?? columns.map((c) => c.key)\n    const lower = debouncedSearch.toLowerCase()\n    return data.filter((row) =>\n      keys.some((k) => String(row[k] ?? \"\").toLowerCase().includes(lower))\n    )\n  }, [data, debouncedSearch, searchKeys, columns])\n\n  // ── Sort ───────────────────────────────────────────────────────────────────\n  const [sortKey, setSortKey] = React.useState<string | null>(null)\n  const [sortDir, setSortDir] = React.useState<SortDir>(null)\n\n  function handleSort(key: string) {\n    if (sortKey !== key) {\n      setSortKey(key)\n      setSortDir(\"asc\")\n    } else if (sortDir === \"asc\") {\n      setSortDir(\"desc\")\n    } else {\n      setSortKey(null)\n      setSortDir(null)\n    }\n  }\n\n  const sortedData = React.useMemo(() => {\n    if (!sortKey || !sortDir) return filteredData\n    return [...filteredData].sort((a, b) => {\n      const av = a[sortKey as keyof T]\n      const bv = b[sortKey as keyof T]\n      const cmp = String(av ?? \"\").localeCompare(String(bv ?? \"\"), undefined, {\n        numeric: true,\n      })\n      return sortDir === \"asc\" ? cmp : -cmp\n    })\n  }, [filteredData, sortKey, sortDir])\n\n  // ── Pagination ─────────────────────────────────────────────────────────────\n  const [page, setPage] = React.useState(1)\n  const totalPages = paginated\n    ? Math.max(1, Math.ceil(sortedData.length / pageSize))\n    : 1\n\n  // Reset page when data changes\n  React.useEffect(() => {\n    setPage(1)\n  }, [debouncedSearch, sortKey, sortDir])\n\n  const displayData = paginated\n    ? sortedData.slice((page - 1) * pageSize, page * pageSize)\n    : sortedData\n\n  // ── Selection ──────────────────────────────────────────────────────────────\n  const [selectedIds, setSelectedIds] = React.useState<Set<string | number>>(\n    new Set()\n  )\n\n  function toggleRow(id: string | number) {\n    setSelectedIds((prev) => {\n      const next = new Set(prev)\n      if (next.has(id)) next.delete(id)\n      else next.add(id)\n      return next\n    })\n  }\n\n  function toggleAll() {\n    if (selectedIds.size === sortedData.length) {\n      setSelectedIds(new Set())\n    } else {\n      setSelectedIds(new Set(sortedData.map((r) => r.id)))\n    }\n  }\n\n  function clearSelection() {\n    setSelectedIds(new Set())\n  }\n\n  React.useEffect(() => {\n    onSelectionChange?.(data.filter((r) => selectedIds.has(r.id)))\n  }, [selectedIds, data, onSelectionChange])\n\n  // ── Inline editing ─────────────────────────────────────────────────────────\n  const [editingCell, setEditingCell] = React.useState<{\n    rowId: string | number\n    colKey: string\n  } | null>(null)\n\n  function handleSaveEdit(\n    rowId: string | number,\n    colKey: string,\n    value: string\n  ) {\n    setData((prev) =>\n      prev.map((row) =>\n        row.id === rowId ? { ...row, [colKey]: value } : row\n      )\n    )\n    setEditingCell(null)\n  }\n\n  // ── Column widths (resizing) ───────────────────────────────────────────────\n  const [colWidths, setColWidths] = React.useState<number[]>(\n    columns.map((c) => c.width ?? 160)\n  )\n\n  const resizeRef = React.useRef<{\n    colIdx: number\n    startX: number\n    startWidth: number\n  } | null>(null)\n\n  function handleResizeStart(colIdx: number, startX: number) {\n    resizeRef.current = {\n      colIdx,\n      startX,\n      startWidth: colWidths[colIdx],\n    }\n\n    function onMouseMove(e: MouseEvent) {\n      if (!resizeRef.current) return\n      const diff = e.clientX - resizeRef.current.startX\n      const newWidth = Math.max(60, resizeRef.current.startWidth + diff)\n      setColWidths((prev) => {\n        const next = [...prev]\n        next[resizeRef.current!.colIdx] = newWidth\n        return next\n      })\n    }\n\n    function onMouseUp() {\n      resizeRef.current = null\n      document.removeEventListener(\"mousemove\", onMouseMove)\n      document.removeEventListener(\"mouseup\", onMouseUp)\n    }\n\n    document.addEventListener(\"mousemove\", onMouseMove)\n    document.addEventListener(\"mouseup\", onMouseUp)\n  }\n\n  // ── Render ─────────────────────────────────────────────────────────────────\n  const allSelected =\n    sortedData.length > 0 && selectedIds.size === sortedData.length\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      {searchable && (\n        <DataTableToolbar\n          searchValue={searchValue}\n          onSearchChange={setSearchValue}\n          totalRows={data.length}\n          filteredRows={filteredData.length}\n        />\n      )}\n\n      <div className=\"overflow-x-auto rounded-lg border border-border bg-card\">\n        <DataTableHeader\n          columns={columns}\n          colWidths={colWidths}\n          sortKey={sortKey}\n          sortDir={sortDir}\n          onSort={handleSort}\n          onResizeStart={handleResizeStart}\n          selectable={selectable}\n          allSelected={allSelected}\n          onToggleAll={toggleAll}\n        />\n        <DataTableBody\n          rows={displayData}\n          columns={columns}\n          colWidths={colWidths}\n          selectable={selectable}\n          selectedIds={selectedIds}\n          onToggleRow={toggleRow}\n          editingCell={editingCell}\n          onStartEdit={(rowId, colKey) => setEditingCell({ rowId, colKey })}\n          onSaveEdit={handleSaveEdit}\n          onCancelEdit={() => setEditingCell(null)}\n        />\n      </div>\n\n      {selectable && (\n        <DataTableBatchBar count={selectedIds.size} onClear={clearSelection} />\n      )}\n\n      {paginated && (\n        <DataTablePagination\n          page={page}\n          totalPages={totalPages}\n          pageSize={pageSize}\n          totalRows={sortedData.length}\n          onPageChange={setPage}\n        />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}