{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "author": "Poyraz Avsever",
  "description": "Poyraz Soft Glass data-table with semantic states and composable variants.",
  "dependencies": [
    "lucide-react@^0.574.0"
  ],
  "registryDependencies": [
    "@poyraz/poyraz-utils",
    "@poyraz/poyraz-theme",
    "@poyraz/button",
    "@poyraz/input",
    "@poyraz/badge",
    "@poyraz/data-table-core"
  ],
  "files": [
    {
      "path": "registry/poyraz/ui/phase7/organisms/data-table.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n  Search,\n  SlidersHorizontal,\n  Check,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/atoms/button\";\nimport { Input } from \"@/components/ui/atoms/input\";\nimport { Badge } from \"@/components/ui/atoms/badge\";\nimport {\n  DataTableCore,\n  getDataTableCellValue,\n  stringifyDataTableValue,\n  type DataTableColumnDef,\n  type DataTableState,\n  type SortDirection,\n} from \"@/components/ui/organisms/data-table-core\";\n\n/* ================================================================== */\n/*  DATA TABLE — Full-featured table with sort, filter, pagination     */\n/* ================================================================== */\n\n/* ── Types ────────────────────────────────────────────────────────── */\n\nexport interface DataTableProps<T> {\n  /** Column definitions */\n  columns: DataTableColumnDef<T>[];\n  /** Data rows */\n  data: T[];\n  /** Unique key extractor for each row */\n  getRowId?: (row: T, index: number) => string;\n  /** Rows per page (default: 10) */\n  pageSize?: number;\n  /** Show pagination (default: true) */\n  pagination?: boolean;\n  /** Show global search (default: true) */\n  searchable?: boolean;\n  /** Search placeholder */\n  searchPlaceholder?: string;\n  /** Enable row selection (default: false) */\n  selectable?: boolean;\n  /** Callback when selection changes */\n  onSelectionChange?: (selectedRows: T[]) => void;\n  /** Show column visibility toggle (default: false) */\n  columnToggle?: boolean;\n  /** Additional class for the wrapper */\n  className?: string;\n  /** Caption for accessibility */\n  caption?: string;\n  /** Empty state message */\n  emptyMessage?: string;\n  loading?: boolean;\n  error?: React.ReactNode;\n  stickyHeader?: boolean;\n  tableMaxHeight?: string | number;\n  density?: \"compact\" | \"default\" | \"spacious\";\n  surface?: \"solid\" | \"soft\" | \"glass\";\n  radius?: \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n  toolbar?: React.ReactNode;\n}\n\n/* ================================================================== */\n/*  MAIN COMPONENT                                                     */\n/* ================================================================== */\n\nfunction DataTableInner<T>(\n  {\n    columns,\n    data,\n    getRowId,\n    pageSize = 10,\n    pagination = true,\n    searchable = true,\n    searchPlaceholder = \"Search...\",\n    selectable = false,\n    onSelectionChange,\n    columnToggle = false,\n    className,\n    caption,\n    emptyMessage = \"No results found.\",\n    loading = false,\n    error,\n    stickyHeader = false,\n    tableMaxHeight,\n    density = \"default\",\n    surface = \"solid\",\n    radius = \"lg\",\n    toolbar,\n  }: DataTableProps<T>,\n  ref: React.ForwardedRef<HTMLDivElement>,\n) {\n  /* ── State ───────────────────────────────────────────────────────── */\n  const [search, setSearch] = React.useState(\"\");\n  const [sortCol, setSortCol] = React.useState<string | null>(null);\n  const [sortDir, setSortDir] = React.useState<SortDirection>(null);\n  const [page, setPage] = React.useState(0);\n  const [selectedIds, setSelectedIds] = React.useState<Set<string>>(new Set());\n  const [hiddenCols, setHiddenCols] = React.useState<Set<string>>(\n    () => new Set(columns.filter((c) => c.hidden).map((c) => c.id)),\n  );\n  const [colToggleOpen, setColToggleOpen] = React.useState(false);\n\n  /* ── Visible columns ─────────────────────────────────────────────── */\n  const visibleColumns = columns.filter((c) => !hiddenCols.has(c.id));\n\n  /* ── Filtered data ───────────────────────────────────────────────── */\n  const filtered = React.useMemo(() => {\n    if (!search.trim()) return data;\n    const q = search.toLowerCase();\n    return data.filter((row) =>\n      columns.some((col) => {\n        const val = getDataTableCellValue(row, col);\n        return stringifyDataTableValue(val).toLowerCase().includes(q);\n      }),\n    );\n  }, [data, search, columns]);\n\n  /* ── Sorted data ─────────────────────────────────────────────────── */\n  const sorted = React.useMemo(() => {\n    if (!sortCol || !sortDir) return filtered;\n    const col = columns.find((c) => c.id === sortCol);\n    if (!col) return filtered;\n    return [...filtered].sort((a, b) => {\n      const aVal = getDataTableCellValue(a, col);\n      const bVal = getDataTableCellValue(b, col);\n      const aStr = stringifyDataTableValue(aVal);\n      const bStr = stringifyDataTableValue(bVal);\n      const aNum = Number(aStr);\n      const bNum = Number(bStr);\n      // Numeric comparison if both are numbers\n      if (!isNaN(aNum) && !isNaN(bNum)) {\n        return sortDir === \"asc\" ? aNum - bNum : bNum - aNum;\n      }\n      // String comparison\n      const cmp = aStr.localeCompare(bStr);\n      return sortDir === \"asc\" ? cmp : -cmp;\n    });\n  }, [filtered, sortCol, sortDir, columns]);\n\n  /* ── Paginated data ──────────────────────────────────────────────── */\n  const totalPages = pagination ? Math.max(1, Math.ceil(sorted.length / pageSize)) : 1;\n  const rows = pagination ? sorted.slice(page * pageSize, (page + 1) * pageSize) : sorted;\n\n  // Reset page when data or search changes\n  React.useEffect(() => {\n    setPage(0);\n  }, [search, data]);\n\n  /* ── Row ID helper ───────────────────────────────────────────────── */\n  const rowId = (row: T, i: number) => (getRowId ? getRowId(row, i) : String(i));\n\n  /* ── Selection ───────────────────────────────────────────────────── */\n  const allPageSelected =\n    rows.length > 0 && rows.every((row, i) => selectedIds.has(rowId(row, page * pageSize + i)));\n\n  const toggleRow = (id: string) => {\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  const toggleAll = () => {\n    setSelectedIds((prev) => {\n      const next = new Set(prev);\n      if (allPageSelected) {\n        rows.forEach((_, i) => next.delete(rowId(rows[i], page * pageSize + i)));\n      } else {\n        rows.forEach((_, i) => next.add(rowId(rows[i], page * pageSize + i)));\n      }\n      return next;\n    });\n  };\n\n  // Notify parent on selection change\n  React.useEffect(() => {\n    if (!selectable || !onSelectionChange) return;\n    const selected = data.filter((row, i) => selectedIds.has(rowId(row, i)));\n    onSelectionChange(selected);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [selectedIds]);\n\n  /* ── Sort handler ────────────────────────────────────────────────── */\n  const handleSort = (colId: string) => {\n    if (sortCol !== colId) {\n      setSortCol(colId);\n      setSortDir(\"asc\");\n    } else if (sortDir === \"asc\") {\n      setSortDir(\"desc\");\n    } else {\n      setSortCol(null);\n      setSortDir(null);\n    }\n  };\n\n  /* ── Column toggle ───────────────────────────────────────────────── */\n  const toggleColumn = (colId: string) => {\n    setHiddenCols((prev) => {\n      const next = new Set(prev);\n      if (next.has(colId)) next.delete(colId);\n      else next.add(colId);\n      return next;\n    });\n  };\n\n  /* ── Render ──────────────────────────────────────────────────────── */\n  return (\n    <div ref={ref} className={cn(\"w-full space-y-4 animate-poyraz-fade-in\", className)}>\n      {/* Toolbar */}\n      {(searchable || columnToggle || (selectable && selectedIds.size > 0)) && (\n        <div className=\"flex flex-col sm:flex-row items-start sm:items-center gap-3\">\n          {searchable && (\n            <div className=\"relative w-full sm:max-w-xs\">\n              <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-placeholder\" />\n              <Input\n                value={search}\n                onChange={(e) => setSearch(e.target.value)}\n                placeholder={searchPlaceholder}\n                className=\"pl-9 h-8 text-sm\"\n              />\n            </div>\n          )}\n\n          <div className=\"flex items-center gap-2 ml-auto\">\n            {selectable && selectedIds.size > 0 && (\n              <Badge variant=\"secondary\" className=\"text-xs animate-poyraz-scale-in\">\n                {selectedIds.size} selected\n              </Badge>\n            )}\n\n            {columnToggle && (\n              <div className=\"relative\">\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  className=\"h-8 gap-1.5 transition-[color,background-color,border-color,transform] duration-[var(--poyraz-motion-duration-fast)] ease-[var(--poyraz-motion-ease-out)] active:scale-[0.98]\"\n                  onClick={() => setColToggleOpen((v) => !v)}\n                >\n                  <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n                  Columns\n                </Button>\n                {colToggleOpen && (\n                  <>\n                    <div className=\"fixed inset-0 z-40\" onClick={() => setColToggleOpen(false)} />\n                    <div className=\"absolute right-0 top-full mt-1 z-50 w-48 bg-background border border-border p-2 space-y-0.5 origin-top-right animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-[var(--poyraz-motion-duration-base)]\">\n                      {columns.map((col) => (\n                        <button\n                          key={col.id}\n                          type=\"button\"\n                          className=\"flex w-full cursor-pointer items-center gap-2 px-2 py-1.5 text-sm transition-colors duration-[var(--poyraz-motion-duration-fast)] hover:bg-muted\"\n                          onClick={() => toggleColumn(col.id)}\n                        >\n                          <span\n                            className={cn(\n                              \"h-4 w-4 border flex items-center justify-center shrink-0 transition-[color,background-color,border-color,transform] duration-[var(--poyraz-motion-duration-fast)] ease-[var(--poyraz-motion-ease-out)]\",\n                              hiddenCols.has(col.id)\n                                ? \"border-border-strong scale-95\"\n                                : \"border-primary bg-primary text-primary-foreground scale-100\",\n                            )}\n                          >\n                            {!hiddenCols.has(col.id) && (\n                              <Check className=\"h-3 w-3 animate-poyraz-scale-in\" />\n                            )}\n                          </span>\n                          {col.header}\n                        </button>\n                      ))}\n                    </div>\n                  </>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n\n      {toolbar}\n\n      {/* Table */}\n      <DataTableCore\n        columns={visibleColumns}\n        data={rows}\n        getRowId={(row, index) => rowId(row, page * pageSize + index)}\n        state={\n          (loading\n            ? \"loading\"\n            : error\n              ? \"error\"\n              : rows.length\n                ? \"populated\"\n                : \"empty\") satisfies DataTableState\n        }\n        emptyContent={emptyMessage}\n        errorContent={error}\n        selectable={selectable}\n        selectedIds={selectedIds}\n        onToggleRow={(id) => toggleRow(id)}\n        onToggleAll={toggleAll}\n        sortColumn={sortCol}\n        sortDirection={sortDir}\n        onSort={handleSort}\n        caption={caption}\n        stickyHeader={stickyHeader}\n        maxHeight={tableMaxHeight}\n        density={density}\n        surface={surface}\n        radius={radius}\n      />\n\n      {/* Pagination */}\n      {pagination && sorted.length > 0 && (\n        <div className=\"flex flex-col sm:flex-row items-center justify-between gap-3\">\n          <p className=\"text-xs text-placeholder\">\n            Showing{\" \"}\n            <span className=\"font-semibold text-muted-foreground\">{page * pageSize + 1}</span>–\n            <span className=\"font-semibold text-muted-foreground\">\n              {Math.min((page + 1) * pageSize, sorted.length)}\n            </span>{\" \"}\n            of <span className=\"font-semibold text-muted-foreground\">{sorted.length}</span> results\n          </p>\n\n          <div className=\"flex items-center gap-1\">\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"h-7 w-7\"\n              disabled={page === 0}\n              onClick={() => setPage(0)}\n              aria-label=\"First page\"\n            >\n              <ChevronsLeft className=\"h-3.5 w-3.5\" />\n            </Button>\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"h-7 w-7\"\n              disabled={page === 0}\n              onClick={() => setPage((p) => Math.max(0, p - 1))}\n              aria-label=\"Previous page\"\n            >\n              <ChevronLeft className=\"h-3.5 w-3.5\" />\n            </Button>\n\n            <span className=\"px-3 text-xs font-semibold text-muted-foreground\">\n              {page + 1} / {totalPages}\n            </span>\n\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"h-7 w-7\"\n              disabled={page >= totalPages - 1}\n              onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}\n              aria-label=\"Next page\"\n            >\n              <ChevronRight className=\"h-3.5 w-3.5\" />\n            </Button>\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"h-7 w-7\"\n              disabled={page >= totalPages - 1}\n              onClick={() => setPage(totalPages - 1)}\n              aria-label=\"Last page\"\n            >\n              <ChevronsRight className=\"h-3.5 w-3.5\" />\n            </Button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\n// Wrap in forwardRef with generics\nconst DataTable = React.forwardRef(DataTableInner) as <T>(\n  props: DataTableProps<T> & { ref?: React.Ref<HTMLDivElement> },\n) => React.ReactElement;\n\nexport { DataTable };\nexport type { DataTableColumnDef } from \"@/components/ui/organisms/data-table-core\";\n",
      "type": "registry:ui",
      "target": "@ui/organisms/data-table.tsx"
    }
  ],
  "meta": {
    "category": "phase-7-composite",
    "phase": "beta"
  },
  "type": "registry:ui"
}