{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "type": "registry:ui",
  "title": "DataTable",
  "description": "Dense SaaS data table with search filtering, multi-row selection, sorting, and pagination.",
  "registryDependencies": [
    "https://ui.arpitbhayani.me/r/aui.json"
  ],
  "files": [
    {
      "path": "ui/data-table.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DataTableColumn<T> {\n  header: React.ReactNode;\n  accessor?: keyof T;\n  render?: (row: T, index: number) => React.ReactNode;\n  sortable?: boolean;\n  align?: \"left\" | \"center\" | \"right\";\n  width?: string | number;\n}\n\nexport interface DataTableProps<T = any> {\n  data: T[];\n  columns: DataTableColumn<T>[];\n  keyField?: keyof T;\n  searchable?: boolean;\n  searchPlaceholder?: string;\n  selectable?: boolean;\n  selectedKeys?: any[];\n  onSelectedKeysChange?: (keys: any[]) => void;\n  renderBulkActions?: (selectedRows: T[]) => React.ReactNode;\n  pageSize?: number;\n  dense?: boolean;\n  emptyText?: string;\n  className?: string;\n}\n\nexport function DataTable<T extends Record<string, any>>({\n  data = [],\n  columns = [],\n  keyField = \"id\" as keyof T,\n  searchable = true,\n  searchPlaceholder = \"Search...\",\n  selectable = false,\n  selectedKeys: controlledSelectedKeys,\n  onSelectedKeysChange,\n  renderBulkActions,\n  pageSize = 10,\n  dense = false,\n  emptyText = \"No records found\",\n  className,\n}: DataTableProps<T>) {\n  const [internalSelectedKeys, setInternalSelectedKeys] = React.useState<any[]>([]);\n  const [search, setSearch] = React.useState(\"\");\n  const [sortKey, setSortKey] = React.useState<keyof T | null>(null);\n  const [sortDir, setSortDir] = React.useState<\"asc\" | \"desc\" | null>(null);\n  const [currentPage, setCurrentPage] = React.useState(1);\n\n  const selectedKeys = controlledSelectedKeys !== undefined ? controlledSelectedKeys : internalSelectedKeys;\n\n  const setSelectedKeys = (keys: any[]) => {\n    if (onSelectedKeysChange) onSelectedKeysChange(keys);\n    else setInternalSelectedKeys(keys);\n  };\n\n  const filteredData = React.useMemo(() => {\n    if (!search.trim()) return data;\n    const q = search.toLowerCase();\n    return data.filter((row) =>\n      columns.some((col) => {\n        if (!col.accessor) return false;\n        const val = row[col.accessor];\n        return val !== null && val !== undefined && String(val).toLowerCase().includes(q);\n      })\n    );\n  }, [data, search, columns]);\n\n  const sortedData = React.useMemo(() => {\n    if (!sortKey || !sortDir) return filteredData;\n    return [...filteredData].sort((a, b) => {\n      const valA = a[sortKey];\n      const valB = b[sortKey];\n      if (valA === valB) return 0;\n      if (valA === null || valA === undefined) return 1;\n      if (valB === null || valB === undefined) return -1;\n      let comp = 0;\n      if (typeof valA === \"number\" && typeof valB === \"number\") comp = valA - valB;\n      else comp = String(valA).localeCompare(String(valB), undefined, { numeric: true });\n      return sortDir === \"asc\" ? comp : -comp;\n    });\n  }, [filteredData, sortKey, sortDir]);\n\n  const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));\n  const page = Math.min(currentPage, totalPages);\n  const paginatedData = React.useMemo(() => {\n    const start = (page - 1) * pageSize;\n    return sortedData.slice(start, start + pageSize);\n  }, [sortedData, page, pageSize]);\n\n  const allPageKeys = React.useMemo(() => paginatedData.map((row) => row[keyField]), [paginatedData, keyField]);\n  const isAllPageSelected = allPageKeys.length > 0 && allPageKeys.every((k) => selectedKeys.includes(k));\n\n  const toggleSelectAll = () => {\n    if (isAllPageSelected) {\n      setSelectedKeys(selectedKeys.filter((k) => !allPageKeys.includes(k)));\n    } else {\n      setSelectedKeys(Array.from(new Set([...selectedKeys, ...allPageKeys])));\n    }\n  };\n\n  const toggleSelectRow = (key: any) => {\n    if (selectedKeys.includes(key)) {\n      setSelectedKeys(selectedKeys.filter((k) => k !== key));\n    } else {\n      setSelectedKeys([...selectedKeys, key]);\n    }\n  };\n\n  const selectedRows = React.useMemo(() => data.filter((r) => selectedKeys.includes(r[keyField])), [data, selectedKeys, keyField]);\n\n  return (\n    <div className={cn(\"space-y-3 w-full\", className)}>\n      <div className=\"flex items-center justify-between gap-4\">\n        {searchable && (\n          <input\n            type=\"text\"\n            className=\"flex h-9 w-64 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-xs outline-hidden focus:ring-1 focus:ring-ring\"\n            placeholder={searchPlaceholder}\n            value={search}\n            onChange={(e) => {\n              setSearch(e.target.value);\n              setCurrentPage(1);\n            }}\n          />\n        )}\n\n        {selectable && selectedKeys.length > 0 && (\n          <div className=\"flex items-center gap-3 px-3 py-1 bg-muted rounded-md text-xs font-mono\">\n            <span className=\"text-primary font-bold\">{selectedKeys.length} selected</span>\n            {renderBulkActions && renderBulkActions(selectedRows)}\n          </div>\n        )}\n      </div>\n\n      <div className=\"rounded-lg border border-border bg-card shadow-xs overflow-auto\">\n        <table className=\"w-full text-sm border-collapse text-left\">\n          <thead className=\"border-b border-border bg-muted/40 font-mono text-xs text-muted-foreground uppercase\">\n            <tr>\n              {selectable && (\n                <th className=\"p-3 w-10 text-center\">\n                  <input\n                    type=\"checkbox\"\n                    checked={isAllPageSelected}\n                    onChange={toggleSelectAll}\n                    className=\"h-3.5 w-3.5 accent-primary\"\n                  />\n                </th>\n              )}\n              {columns.map((col, idx) => (\n                <th\n                  key={idx}\n                  style={{ width: col.width, textAlign: col.align || \"left\" }}\n                  className={cn(\"p-3 font-semibold\", col.sortable && \"cursor-pointer select-none hover:text-foreground\")}\n                  onClick={() => {\n                    if (!col.sortable || !col.accessor) return;\n                    if (sortKey === col.accessor) {\n                      if (sortDir === \"asc\") setSortDir(\"desc\");\n                      else if (sortDir === \"desc\") { setSortDir(null); setSortKey(null); }\n                      else setSortDir(\"asc\");\n                    } else {\n                      setSortKey(col.accessor);\n                      setSortDir(\"asc\");\n                    }\n                  }}\n                >\n                  {col.header}\n                  {col.sortable && col.accessor === sortKey && (\n                    <span className=\"ml-1 text-primary\">{sortDir === \"asc\" ? \"▲\" : \"▼\"}</span>\n                  )}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {paginatedData.length === 0 ? (\n              <tr>\n                <td colSpan={columns.length + (selectable ? 1 : 0)} className=\"p-8 text-center text-xs text-muted-foreground\">\n                  {emptyText}\n                </td>\n              </tr>\n            ) : (\n              paginatedData.map((row, rIdx) => {\n                const rowKey = row[keyField] ?? rIdx;\n                const isSelected = selectedKeys.includes(rowKey);\n                return (\n                  <tr key={String(rowKey)} className={cn(\"border-b border-border/60 hover:bg-muted/30 transition-colors\", isSelected && \"bg-muted/40\")}>\n                    {selectable && (\n                      <td className=\"p-3 text-center\">\n                        <input\n                          type=\"checkbox\"\n                          checked={isSelected}\n                          onChange={() => toggleSelectRow(rowKey)}\n                          className=\"h-3.5 w-3.5 accent-primary\"\n                        />\n                      </td>\n                    )}\n                    {columns.map((col, cIdx) => (\n                      <td key={cIdx} style={{ textAlign: col.align || \"left\" }} className={cn(\"p-3 text-foreground\", dense && \"py-2\")}>\n                        {col.render ? col.render(row, rIdx) : col.accessor ? row[col.accessor] : null}\n                      </td>\n                    ))}\n                  </tr>\n                );\n              })\n            )}\n          </tbody>\n        </table>\n      </div>\n\n      {totalPages > 1 && (\n        <div className=\"flex items-center justify-between text-xs font-mono text-muted-foreground\">\n          <span>Page {page} of {totalPages} ({sortedData.length} records)</span>\n          <div className=\"flex gap-2\">\n            <button\n              type=\"button\"\n              disabled={page <= 1}\n              onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}\n              className=\"px-2.5 py-1 rounded border border-input bg-background hover:bg-muted disabled:opacity-40\"\n            >\n              Previous\n            </button>\n            <button\n              type=\"button\"\n              disabled={page >= totalPages}\n              onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}\n              className=\"px-2.5 py-1 rounded border border-input bg-background hover:bg-muted disabled:opacity-40\"\n            >\n              Next\n            </button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ]
}