{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-data-table",
  "title": "Data Table",
  "description": "A high-performance virtualized data table with built-in toolbar, pagination, and sorting capabilities.",
  "dependencies": [
    "@tanstack/react-table",
    "@tanstack/react-virtual",
    "lucide-react"
  ],
  "registryDependencies": [
    "@lema-ds/button",
    "@lema-ds/skeleton",
    "@lema-ds/ds-pagination",
    "@lema-ds/ds-search-bar",
    "@lema-ds/ui-i18n",
    "@lema-ds/format-utils",
    "@lema-ds/ds-locale-provider"
  ],
  "files": [
    {
      "path": "components/ds/ds-data-table.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  type ColumnDef,\n  type RowData,\n  columnFilteringFeature,\n  columnResizingFeature,\n  columnSizingFeature,\n  createFilteredRowModel,\n  createPaginatedRowModel,\n  createSortedRowModel,\n  filterFn_includesString,\n  flexRender,\n  globalFilteringFeature,\n  metaHelper,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  tableFeatures,\n  useTable,\n  type PaginationState,\n  type RowSelectionState,\n  type SortingState,\n} from \"@tanstack/react-table\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport {\n  ArrowUp,\n  ArrowDown,\n  ChevronsUpDown,\n  Download,\n  Table2,\n} from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { type UILocale, UI_I18N } from \"@/lib/ui-i18n\"\nimport { useOptionalUILocale } from \"@/components/ui/ds-locale-provider\"\nimport { formatValue as fmtValue } from \"@/lib/format-utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n} from \"@/components/ui/ds-pagination\"\nimport { SearchBar } from \"@/components/ui/ds-search-bar\"\n\n// ── Table Features ─────────────────────────────────────────────────────────\n// Explicit v9 feature registration. Column layout/display hints live in the\n// `columnMeta` type-only slot instead of global `ColumnMeta` declaration\n// merging.\n\nexport interface DataTableColumnMeta {\n  align?: \"left\" | \"center\" | \"right\"\n  width?: number\n  flexGrow?: number\n  wrap?: boolean\n  format?: \"percent\" | \"currency\" | \"number\" | \"money\"\n  locale?: string\n  formatOptions?: Intl.NumberFormatOptions\n}\n\nconst dataTableFeatures = tableFeatures({\n  columnFilteringFeature,\n  globalFilteringFeature,\n  rowSortingFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  columnSizingFeature,\n  columnResizingFeature,\n  filteredRowModel: createFilteredRowModel(),\n  sortedRowModel: createSortedRowModel(),\n  paginatedRowModel: createPaginatedRowModel(),\n  filterFns: { includesString: filterFn_includesString },\n  columnMeta: metaHelper<DataTableColumnMeta>(),\n})\n\ntype DataTableFeatures = typeof dataTableFeatures\n\n/** `ColumnDef` pre-bound to the DataTable's registered features. */\nexport type DataTableColumnDef<\n  TData extends RowData,\n  TValue = unknown,\n> = ColumnDef<DataTableFeatures, TData, TValue>\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\n/**\n * Convenience column descriptor. Pass to `col()` to produce a `ColumnDef`.\n * Use `ColumnDef<T>` directly when you need the full TanStack Table API.\n */\nexport interface DataTableColumn<T extends RowData> {\n  key: keyof T & string\n  label: string\n  width?: number\n  flexGrow?: number\n  format?: \"percent\" | \"currency\" | \"number\" | \"money\"\n  locale?: string\n  formatOptions?: Intl.NumberFormatOptions\n  wrap?: boolean\n  sortable?: boolean\n  align?: \"left\" | \"center\" | \"right\"\n  cell?: (value: unknown, row: T) => React.ReactNode\n}\n\nexport interface DataTableLabels {\n  searchPlaceholder?: string\n  noData?: string\n  noDataDescription?: string\n  pagination?: {\n    of?: string\n    pluralItemName?: string\n    rowsPerPage?: string\n    prev?: string\n    next?: string\n  }\n  actions?: {\n    download?: string\n    clearSearch?: string\n  }\n  selection?: {\n    selectAll?: string\n    selectRow?: string\n  }\n}\n\nexport interface DataTableProps<\n  TData extends RowData,\n> extends React.HTMLAttributes<HTMLDivElement> {\n  columns: DataTableColumnDef<TData>[]\n  data: TData[]\n\n  // Header\n  title?: string\n  subtitle?: string\n\n  // Footer\n  footer?: React.ReactNode\n\n  // Layout\n  rowHeight?: number\n  height?: number\n  width?: string | number\n  size?: \"compact\" | \"default\"\n  textSize?: \"xs\" | \"sm\" | \"md\" | \"lg\"\n  rounded?: boolean\n  paginationRounded?: \"full\" | \"light\" | \"none\"\n\n  // Features\n  loading?: boolean\n  showSearch?: boolean\n  manualSorting?: boolean\n  sorting?: SortingState\n  onSortingChange?: (sorting: SortingState) => void\n  pagination?: boolean\n  manualPagination?: boolean\n  pageCount?: number\n  rowCount?: number\n  defaultPageSize?: number\n  pageIndex?: number\n  defaultGlobalFilter?: string\n  pageSizeOptions?: number[]\n  selectRows?: boolean\n  stickyColumns?: number\n  onPageChange?: (pageIndex: number) => void\n\n  // Toolbar\n  toolbar?: React.ReactNode\n  showDownload?: boolean\n  onDownload?: () => void\n  voiceSearch?: boolean\n  onVoiceStart?: () => void\n  onVoiceEnd?: () => void\n  onVoiceError?: (error: string) => void\n\n  // Handlers\n  onRowClick?: (row: TData) => void\n  onSelectedRowsChange?: (rows: TData[]) => void\n\n  // Labels\n  locale?: UILocale\n  labels?: DataTableLabels\n\n  // Infinite scroll (non-paginated mode)\n  hasMore?: boolean\n  onLoadMore?: () => void\n\n  ariaLabel?: string\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction formatValue(\n  val: unknown,\n  format?: string,\n  locale?: string,\n  formatOptions?: Intl.NumberFormatOptions\n): string {\n  if (val == null) return \"\"\n  const n = Number(val)\n  switch (format) {\n    case \"percent\":\n      return fmtValue(n, \"percent\", {\n        decimals: formatOptions?.minimumFractionDigits ?? 2,\n        locale,\n      })\n    case \"currency\":\n    case \"money\":\n      return fmtValue(n, \"currency\", {\n        currency: formatOptions?.currency ?? \"BRL\",\n        decimals:\n          formatOptions?.minimumFractionDigits ??\n          formatOptions?.maximumFractionDigits ??\n          2,\n        locale: locale ?? \"en-US\",\n      })\n    case \"number\":\n      return fmtValue(n, \"float\", {\n        decimals: formatOptions?.maximumFractionDigits ?? 2,\n        locale,\n      })\n    default:\n      return String(val)\n  }\n}\n\nfunction sortingStatesEqual(a: SortingState, b: SortingState): boolean {\n  if (a === b) return true\n  if (a.length !== b.length) return false\n  return a.every((s, i) => s.id === b[i].id && s.desc === b[i].desc)\n}\n\n/**\n * Converts a simple `DataTableColumn` descriptor into a TanStack Table `ColumnDef`.\n *\n * @example\n * const columns: ColumnDef<Student>[] = [\n *   col({ key: \"name\",   label: \"Name\",   sortable: true }),\n *   col({ key: \"course\", label: \"Course\", width: 220 }),\n *   col({ key: \"gpa\",    label: \"GPA\",    align: \"center\", sortable: true }),\n *   col({ key: \"status\", label: \"Status\", cell: (_, row) => <Badge>{row.status}</Badge> }),\n * ]\n */\nexport function col<T extends RowData>(\n  def: DataTableColumn<T>\n): DataTableColumnDef<T> {\n  return {\n    id: def.key,\n    accessorKey: def.key,\n    header: def.label,\n    enableSorting: def.sortable ?? false,\n    enableColumnFilter: false,\n    meta: {\n      align: def.align,\n      width: def.width,\n      flexGrow: def.flexGrow,\n      wrap: def.wrap,\n      format: def.format,\n      locale: def.locale,\n      formatOptions: def.formatOptions,\n    },\n    cell: def.cell\n      ? ({ row, getValue }) => def.cell!(getValue(), row.original)\n      : ({ getValue, column }) =>\n          formatValue(\n            getValue(),\n            column.columnDef.meta?.format,\n            column.columnDef.meta?.locale,\n            column.columnDef.meta?.formatOptions\n          ),\n  }\n}\n\n// ── Column sizing ──────────────────────────────────────────────────────────\n\nfunction colStyle(\n  meta: DataTableColumnMeta | undefined,\n  override?: number\n): React.CSSProperties {\n  const w = override ?? meta?.width\n  return {\n    flexGrow: w ? 0 : (meta?.flexGrow ?? 1),\n    flexShrink: w ? 0 : 1,\n    flexBasis: w ? `${w}px` : \"auto\",\n    width: w ? `${w}px` : \"auto\",\n    minWidth: w ? `${w}px` : \"100px\",\n  }\n}\n\n// ── Table Primitives ───────────────────────────────────────────────────────\n// Exported for consumers who need a custom table layout without DataTable's\n// state management and virtualization.\n\nexport const Table = React.forwardRef<\n  HTMLTableElement,\n  React.TableHTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <table ref={ref} className={cn(\"w-full text-sm\", className)} {...props} />\n))\nTable.displayName = \"Table\"\n\nexport const TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement> & { sticky?: boolean }\n>(({ className, sticky, ...props }, ref) => (\n  <thead\n    ref={ref}\n    className={cn(\n      \"border-b border-border bg-muted transition-colors duration-200\",\n      sticky && \"sticky top-0 z-10\",\n      className\n    )}\n    {...props}\n  />\n))\nTableHeader.displayName = \"TableHeader\"\n\nexport const TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-b-0\", className)}\n    {...props}\n  />\n))\nTableBody.displayName = \"TableBody\"\n\nexport const TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement> & { clickable?: boolean }\n>(({ className, clickable, onClick, ...props }, ref) => (\n  <tr\n    ref={ref}\n    tabIndex={clickable ? 0 : undefined}\n    role={clickable ? \"button\" : undefined}\n    onKeyDown={\n      clickable\n        ? (e) => {\n            if (e.key === \"Enter\" || e.key === \" \") {\n              e.preventDefault()\n              onClick?.(e as unknown as React.MouseEvent<HTMLTableRowElement>)\n            }\n          }\n        : undefined\n    }\n    className={cn(\n      \"border-b border-border/40 transition-colors duration-150 hover:bg-muted/40\",\n      clickable && \"cursor-pointer\",\n      className\n    )}\n    onClick={onClick}\n    {...props}\n  />\n))\nTableRow.displayName = \"TableRow\"\n\nexport const TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"bg-inherit px-4 py-3 text-left align-middle text-xs font-medium tracking-wide text-muted-foreground uppercase\",\n      className\n    )}\n    {...props}\n  />\n))\nTableHead.displayName = \"TableHead\"\n\nexport const TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      \"px-4 py-3 align-middle text-sm text-foreground tabular-nums\",\n      className\n    )}\n    {...props}\n  />\n))\nTableCell.displayName = \"TableCell\"\n\n// ── Skeleton ───────────────────────────────────────────────────────────────\n\nconst SKELETON_WIDTHS = [72, 88, 56, 78, 92, 62, 82, 68, 52, 80]\n\ninterface DataTableSkeletonProps {\n  columns: ReadonlyArray<{ meta?: DataTableColumnMeta }>\n  rowCount: number\n  hasTitle?: boolean\n  hasSubtitle?: boolean\n  hasFooter?: boolean\n  showSearch?: boolean\n  hasToolbar?: boolean\n  pagination?: boolean\n  rounded?: boolean\n  className?: string\n}\n\nfunction DataTableSkeleton({\n  columns,\n  rowCount,\n  hasTitle,\n  hasSubtitle,\n  hasFooter,\n  showSearch,\n  hasToolbar,\n  pagination,\n  rounded = true,\n  className,\n}: DataTableSkeletonProps) {\n  return (\n    <div className={cn(\"flex w-full flex-col gap-4\", className)}>\n      {(hasTitle || hasSubtitle) && (\n        <div className=\"flex flex-col gap-1.5\">\n          {hasTitle && (\n            <Skeleton\n              className=\"h-5 w-44 rounded-md\"\n              style={{ animationDelay: \"0s\" }}\n            />\n          )}\n          {hasSubtitle && (\n            <Skeleton\n              className=\"h-3.5 w-64 rounded-md\"\n              style={{ animationDelay: \"0.05s\" }}\n            />\n          )}\n        </div>\n      )}\n\n      {(hasToolbar || showSearch) && (\n        <div className=\"flex items-center justify-between gap-4\">\n          <div className=\"flex items-center gap-2\">\n            {hasToolbar && (\n              <Skeleton\n                className=\"h-9 w-28 rounded-md\"\n                style={{ animationDelay: \"0.1s\" }}\n              />\n            )}\n          </div>\n          {showSearch && (\n            <Skeleton\n              className=\"h-8 w-72 rounded-md\"\n              style={{ animationDelay: \"0.15s\" }}\n            />\n          )}\n        </div>\n      )}\n\n      <div\n        className={cn(\n          \"w-full overflow-hidden border border-border shadow-sm\",\n          rounded && \"rounded-xl\"\n        )}\n      >\n        {/* Header */}\n        <div className=\"flex border-b border-border bg-muted px-1\">\n          {columns.map((col, i) => (\n            <div\n              key={i}\n              className=\"flex items-center px-3 py-3\"\n              style={colStyle(col.meta)}\n            >\n              <Skeleton\n                className=\"h-2.5 rounded-sm\"\n                style={{\n                  width: `${SKELETON_WIDTHS[i % SKELETON_WIDTHS.length] * 0.45}px`,\n                  animationDelay: `${0.2 + i * 0.04}s`,\n                }}\n              />\n            </div>\n          ))}\n        </div>\n\n        {/* Rows */}\n        {Array.from({ length: rowCount }).map((_, r) => (\n          <div\n            key={r}\n            className=\"flex border-b border-border/40 px-1 last:border-b-0\"\n          >\n            {columns.map((col, c) => {\n              const pct =\n                SKELETON_WIDTHS[\n                  (r * columns.length + c) % SKELETON_WIDTHS.length\n                ]\n              return (\n                <div\n                  key={c}\n                  className=\"flex items-center px-3 py-3\"\n                  style={colStyle(col.meta)}\n                >\n                  <Skeleton\n                    className=\"h-4 rounded-sm\"\n                    style={{\n                      width: `${pct}%`,\n                      animationDelay: `${0.25 + r * 0.04 + c * 0.02}s`,\n                    }}\n                  />\n                </div>\n              )\n            })}\n          </div>\n        ))}\n\n        {pagination && (\n          <div className=\"flex items-center justify-between border-t border-border px-4 py-3\">\n            <Skeleton\n              className=\"h-4 w-32 rounded-md\"\n              style={{ animationDelay: \"0.55s\" }}\n            />\n            <div className=\"flex items-center gap-2\">\n              <Skeleton\n                className=\"size-8 rounded-md\"\n                style={{ animationDelay: \"0.6s\" }}\n              />\n              <Skeleton\n                className=\"h-4 w-20 rounded-md\"\n                style={{ animationDelay: \"0.65s\" }}\n              />\n              <Skeleton\n                className=\"size-8 rounded-md\"\n                style={{ animationDelay: \"0.7s\" }}\n              />\n            </div>\n          </div>\n        )}\n      </div>\n\n      {hasFooter && (\n        <div className=\"flex items-center gap-2 px-1\">\n          <Skeleton\n            className=\"h-3.5 w-48 rounded-md\"\n            style={{ animationDelay: \"0.75s\" }}\n          />\n          <Skeleton\n            className=\"ml-auto h-3.5 w-24 rounded-md\"\n            style={{ animationDelay: \"0.8s\" }}\n          />\n        </div>\n      )}\n    </div>\n  )\n}\n\n// ── Empty State ────────────────────────────────────────────────────────────\n\nfunction DataTableEmpty({\n  message,\n  description,\n}: {\n  message: string\n  description?: string\n}) {\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      className=\"flex flex-col items-center justify-center gap-4 py-16\"\n    >\n      <div className=\"flex size-14 items-center justify-center rounded-xl border border-dashed border-border bg-muted/50\">\n        <Table2 className=\"size-7 text-muted-foreground\" />\n      </div>\n      <div className=\"text-center\">\n        <p className=\"text-sm font-medium text-foreground\">{message}</p>\n        {description && (\n          <p className=\"mt-0.5 text-xs text-muted-foreground\">{description}</p>\n        )}\n      </div>\n    </div>\n  )\n}\n\n// ── Pagination ─────────────────────────────────────────────────────────────\n\nfunction buildPageRange(\n  currentPage: number,\n  totalPages: number\n): (number | \"ellipsis\")[] {\n  if (totalPages <= 7) {\n    return Array.from({ length: totalPages }, (_, i) => i + 1)\n  }\n  const left = currentPage - 1\n  const right = currentPage + 1\n  const pages: number[] = []\n  for (let i = 1; i <= totalPages; i++) {\n    if (i === 1 || i === totalPages || (i >= left && i <= right)) {\n      pages.push(i)\n    }\n  }\n  const range: (number | \"ellipsis\")[] = []\n  let prev: number | null = null\n  for (const p of pages) {\n    if (prev !== null) {\n      if (p - prev === 2) range.push(prev + 1)\n      else if (p - prev > 2) range.push(\"ellipsis\")\n    }\n    range.push(p)\n    prev = p\n  }\n  return range\n}\n\ninterface PaginationBarProps {\n  pageIndex: number\n  pageCount: number\n  totalRows: number\n  pageSize: number\n  pageSizeOptions?: number[]\n  canPrev: boolean\n  canNext: boolean\n  onPrev: () => void\n  onNext: () => void\n  onPageChange: (index: number) => void\n  onPageSizeChange?: (size: number) => void\n  itemLabel: string\n  rowsPerPageLabel: string\n  ofLabel: string\n  prevLabel: string\n  nextLabel: string\n  rounded?: \"full\" | \"light\" | \"none\"\n}\n\nfunction PaginationBar({\n  pageIndex,\n  pageCount,\n  totalRows,\n  pageSize,\n  pageSizeOptions,\n  canPrev,\n  canNext,\n  onPrev,\n  onNext,\n  onPageChange,\n  onPageSizeChange,\n  itemLabel,\n  rowsPerPageLabel,\n  ofLabel,\n  prevLabel,\n  nextLabel,\n  rounded = \"full\",\n}: PaginationBarProps) {\n  const start = totalRows === 0 ? 0 : pageIndex * pageSize + 1\n  const end = Math.min((pageIndex + 1) * pageSize, totalRows)\n  const pageRange = buildPageRange(pageIndex + 1, pageCount)\n\n  return (\n    <div className=\"flex flex-wrap items-center justify-between gap-2 border-t border-border bg-card px-4 py-2\">\n      <div className=\"flex items-center gap-4\">\n        <p className=\"text-xs text-muted-foreground\">\n          {totalRows === 0\n            ? `0 ${itemLabel}`\n            : `${start}–${end} ${ofLabel} ${totalRows} ${itemLabel}`}\n        </p>\n\n        {pageSizeOptions && onPageSizeChange && (\n          <div className=\"flex items-center gap-2\">\n            <label\n              htmlFor=\"dt-page-size\"\n              className=\"text-xs text-muted-foreground\"\n            >\n              {rowsPerPageLabel}\n            </label>\n            <select\n              id=\"dt-page-size\"\n              value={pageSize}\n              onChange={(e) => onPageSizeChange(Number(e.target.value))}\n              className=\"h-8 min-h-[44px] rounded-md border border-border bg-background px-2 text-xs text-foreground focus:ring-1 focus:ring-ring focus:outline-none\"\n            >\n              {pageSizeOptions.map((s) => (\n                <option key={s} value={s}>\n                  {s}\n                </option>\n              ))}\n            </select>\n          </div>\n        )}\n      </div>\n\n      <Pagination className=\"m-0 w-auto justify-end\">\n        <PaginationContent>\n          <PaginationItem>\n            <PaginationPrevious\n              href=\"#\"\n              text={prevLabel}\n              rounded={rounded}\n              onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                e.preventDefault()\n                onPrev()\n              }}\n              aria-disabled={!canPrev}\n              className={cn(\n                \"min-h-[44px] min-w-[44px] hover:bg-primary/10\",\n                !canPrev && \"pointer-events-none opacity-50\"\n              )}\n            />\n          </PaginationItem>\n\n          {pageRange.map((p, i) =>\n            p === \"ellipsis\" ? (\n              <PaginationItem key={`ell-${i}`} className=\"hidden sm:flex\">\n                <PaginationEllipsis />\n              </PaginationItem>\n            ) : (\n              <PaginationItem key={p} className=\"hidden sm:flex\">\n                <PaginationLink\n                  href=\"#\"\n                  isActive={p === pageIndex + 1}\n                  rounded={rounded}\n                  onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                    e.preventDefault()\n                    onPageChange(p - 1)\n                  }}\n                  className={cn(\n                    \"min-h-[44px] min-w-[44px]\",\n                    p === pageIndex + 1\n                      ? \"border-transparent bg-primary/15 text-primary hover:bg-primary/20\"\n                      : \"hover:bg-primary/10\"\n                  )}\n                >\n                  {p}\n                </PaginationLink>\n              </PaginationItem>\n            )\n          )}\n\n          <PaginationItem>\n            <PaginationNext\n              href=\"#\"\n              text={nextLabel}\n              rounded={rounded}\n              onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {\n                e.preventDefault()\n                onNext()\n              }}\n              aria-disabled={!canNext}\n              className={cn(\n                \"min-h-[44px] min-w-[44px] hover:bg-primary/10\",\n                !canNext && \"pointer-events-none opacity-50\"\n              )}\n            />\n          </PaginationItem>\n        </PaginationContent>\n      </Pagination>\n    </div>\n  )\n}\n\n// ── Cell size presets ──────────────────────────────────────────────────────\n\nconst SIZE_PRESETS = {\n  xs: { pad: \"px-2 py-1\", font: \"text-xs\", rowH: 32 },\n  sm: { pad: \"px-3 py-2\", font: \"text-xs\", rowH: 36 },\n  md: { pad: \"px-4 py-2.5\", font: \"text-sm\", rowH: 40 },\n  lg: { pad: \"px-4 py-3\", font: \"text-sm\", rowH: 48 },\n} as const\n\n// ── DataTable ──────────────────────────────────────────────────────────────\n\nexport function DataTable<TData extends RowData>({\n  columns,\n  data,\n  title,\n  subtitle,\n  footer,\n  rowHeight,\n  height = 400,\n  loading = false,\n  width,\n  size = \"default\",\n  textSize: _textSize,\n  rounded = true,\n  paginationRounded,\n  showSearch = false,\n  manualSorting = false,\n  sorting,\n  onSortingChange,\n  pagination = false,\n  manualPagination = false,\n  pageCount,\n  rowCount,\n  defaultPageSize = 10,\n  defaultGlobalFilter = \"\",\n  pageSizeOptions,\n  selectRows = false,\n  stickyColumns = 0,\n  onPageChange,\n  pageIndex,\n  toolbar,\n  showDownload = false,\n  onDownload,\n  voiceSearch = false,\n  onVoiceStart,\n  onVoiceEnd,\n  onVoiceError,\n  onRowClick,\n  onSelectedRowsChange,\n  hasMore = false,\n  onLoadMore,\n  locale: localeProp,\n  labels,\n  ariaLabel,\n  className,\n  ...props\n}: DataTableProps<TData>) {\n  const locale = useOptionalUILocale(localeProp)\n  const textSize = _textSize ?? (size === \"compact\" ? \"sm\" : \"md\")\n  const { pad, font, rowH } = SIZE_PRESETS[textSize]\n  const actualRowHeight = rowHeight ?? rowH\n\n  // ── Labels ──\n\n  const i18n = locale ? UI_I18N[locale] : null\n\n  const l = {\n    searchPlaceholder:\n      labels?.searchPlaceholder ??\n      i18n?.dataTable.searchPlaceholder ??\n      \"Search...\",\n    noData: labels?.noData ?? i18n?.dataTable.noData ?? \"No data found\",\n    noDataDescription:\n      labels?.noDataDescription ??\n      i18n?.dataTable.noDataDescription ??\n      \"Data will appear here once available.\",\n    pagination: {\n      of: labels?.pagination?.of ?? i18n?.dataTable.of ?? \"of\",\n      pluralItemName:\n        labels?.pagination?.pluralItemName ?? i18n?.dataTable.rows ?? \"rows\",\n      rowsPerPage:\n        labels?.pagination?.rowsPerPage ??\n        i18n?.dataTable.rowsPerPage ??\n        \"Rows per page\",\n      prev: labels?.pagination?.prev ?? i18n?.pagination.previous ?? \"Previous\",\n      next: labels?.pagination?.next ?? i18n?.pagination.next ?? \"Next\",\n    },\n    actions: {\n      download: labels?.actions?.download ?? \"Export\",\n      clearSearch:\n        labels?.actions?.clearSearch ??\n        i18n?.combobox.clearSearch ??\n        \"Clear search\",\n    },\n    selection: {\n      selectAll: labels?.selection?.selectAll ?? \"Select all\",\n      selectRow: labels?.selection?.selectRow ?? \"Select row\",\n    },\n  }\n\n  // ── TanStack Table state ──\n\n  const [sorting_, setSorting] = React.useState<SortingState>(sorting ?? [])\n  const [globalFilter, setGlobalFilter] = React.useState(defaultGlobalFilter)\n  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})\n  const [pagination_, setPagination] = React.useState<PaginationState>({\n    pageIndex: pageIndex ?? 0,\n    pageSize: defaultPageSize,\n  })\n\n  // Reset page when global filter changes\n  const handleGlobalFilter = React.useCallback((value: string) => {\n    setGlobalFilter(value)\n    setPagination((p) => ({ ...p, pageIndex: 0 }))\n  }, [])\n\n  // Sync internal pagination when external pageIndex prop changes\n  React.useEffect(() => {\n    if (pageIndex !== undefined && pageIndex !== pagination_.pageIndex) {\n      setPagination((prev) => ({ ...prev, pageIndex }))\n    }\n  }, [pageIndex]) // eslint-disable-line react-hooks/exhaustive-deps\n\n  // Sync internal sorting when external `sorting` prop changes\n  React.useEffect(() => {\n    if (sorting !== undefined && !sortingStatesEqual(sorting, sorting_)) {\n      setSorting(sorting)\n    }\n  }, [sorting]) // eslint-disable-line react-hooks/exhaustive-deps\n\n  // ── Selection column ──\n\n  const selectionColumn = React.useMemo<DataTableColumnDef<TData>>(\n    () => ({\n      id: \"__select__\",\n      meta: { width: 48 },\n      enableSorting: false,\n      header: ({ table }) => (\n        <input\n          type=\"checkbox\"\n          aria-label={l.selection.selectAll}\n          checked={table.getIsAllPageRowsSelected()}\n          ref={(el) => {\n            if (el)\n              el.indeterminate =\n                table.getIsSomePageRowsSelected() &&\n                !table.getIsAllPageRowsSelected()\n          }}\n          onChange={table.getToggleAllPageRowsSelectedHandler()}\n          className=\"size-4 cursor-pointer accent-primary\"\n        />\n      ),\n      cell: ({ row }) => (\n        <input\n          type=\"checkbox\"\n          aria-label={`${l.selection.selectRow} ${row.index + 1}`}\n          checked={row.getIsSelected()}\n          disabled={!row.getCanSelect()}\n          onChange={row.getToggleSelectedHandler()}\n          onClick={(e) => e.stopPropagation()}\n          className=\"size-4 cursor-pointer accent-primary\"\n        />\n      ),\n    }),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    []\n  )\n\n  const tableColumns = React.useMemo(\n    () => (selectRows ? [selectionColumn, ...columns] : columns),\n    [selectRows, selectionColumn, columns]\n  )\n\n  // ── useTable ──\n\n  const sortingConfig = React.useMemo(() => {\n    if (!manualSorting) return {}\n    return { manualSorting: true }\n  }, [manualSorting])\n\n  // Notify parent of sorting changes after render (never during render)\n  const prevSortingRef = React.useRef(sorting_)\n  React.useEffect(() => {\n    if (!manualSorting || !onSortingChange) return\n    if (!sortingStatesEqual(prevSortingRef.current, sorting_)) {\n      prevSortingRef.current = sorting_\n      onSortingChange(sorting_)\n    }\n  }, [manualSorting, onSortingChange, sorting_])\n\n  const paginationConfig = React.useMemo(() => {\n    if (!pagination) return {}\n    if (manualPagination) {\n      return {\n        manualPagination: true,\n        // -1 = TanStack \"unknown total, always allow next\" — used while meta is loading\n        pageCount: pageCount ?? -1,\n        ...(rowCount != null && { rowCount }),\n        onPaginationChange: setPagination,\n      }\n    }\n    return {\n      onPaginationChange: setPagination,\n    }\n  }, [pagination, manualPagination, pageCount, rowCount])\n\n  // Notify parent of pagination changes after render (never during render)\n  const prevPageRef = React.useRef(pagination_.pageIndex)\n  React.useEffect(() => {\n    if (!manualPagination || !onPageChange || !pagination) return\n    if (prevPageRef.current !== pagination_.pageIndex) {\n      prevPageRef.current = pagination_.pageIndex\n      onPageChange(pagination_.pageIndex)\n    }\n  }, [manualPagination, onPageChange, pagination, pagination_.pageIndex])\n\n  const table = useTable({\n    features: dataTableFeatures,\n    data,\n    columns: tableColumns,\n    state: {\n      sorting: sorting_,\n      globalFilter,\n      rowSelection,\n      ...(pagination && { pagination: pagination_ }),\n    },\n    onSortingChange: setSorting,\n    onGlobalFilterChange: handleGlobalFilter,\n    onRowSelectionChange: setRowSelection,\n    ...sortingConfig,\n    ...paginationConfig,\n    enableRowSelection: selectRows,\n    globalFilterFn: \"includesString\",\n    autoResetPageIndex: !manualPagination,\n    columnResizeMode: \"onChange\",\n    enableColumnResizing: true,\n  })\n\n  // ── Selection callback ──\n\n  React.useEffect(() => {\n    if (!onSelectedRowsChange) return\n    onSelectedRowsChange(\n      table.getSelectedRowModel().rows.map((r) => r.original)\n    )\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [rowSelection])\n\n  // ── Virtualization ──\n\n  const parentRef = React.useRef<HTMLDivElement>(null)\n  // `paginatedRowModel` is always registered (features are static), so when\n  // pagination is off we read the pre-paginated (sorted + filtered) model\n  // instead of letting the default page size silently slice the rows.\n  const rows = pagination\n    ? table.getRowModel().rows\n    : table.getPrePaginatedRowModel().rows\n\n  // eslint-disable-next-line react-hooks/incompatible-library\n  const virtualizer = useVirtualizer({\n    count: rows.length,\n    getScrollElement: () => parentRef.current,\n    estimateSize: () => actualRowHeight,\n    overscan: 10,\n  })\n\n  const virtualItems = virtualizer.getVirtualItems()\n\n  // ── Infinite scroll trigger ──\n\n  React.useEffect(() => {\n    if (!hasMore || loading || pagination) return\n    const last = virtualItems[virtualItems.length - 1]\n    if (last && last.index >= rows.length - 5) onLoadMore?.()\n  }, [virtualItems, rows.length, hasMore, loading, pagination, onLoadMore])\n\n  // ── Sticky column left offsets ──\n\n  const stickyLefts = React.useMemo(() => {\n    const offsets: number[] = []\n    let acc = selectRows ? 48 : 0\n    for (let i = 0; i < stickyColumns; i++) {\n      offsets.push(acc)\n      const col = columns[i]\n      acc += (col?.meta as { width?: number })?.width ?? 100\n    }\n    return offsets\n  }, [stickyColumns, columns, selectRows])\n\n  // ── Min table width ──\n\n  const minTableWidth = React.useMemo(() => {\n    let total = selectRows ? 48 : 0\n    tableColumns.forEach((col) => {\n      if (col.id === \"__select__\") return\n      total += (col.meta as { width?: number } | undefined)?.width ?? 100\n    })\n    return total\n  }, [tableColumns, selectRows])\n\n  // ── Skeleton row count ──\n\n  const skeletonRowCount = Math.max(4, Math.floor(height / actualRowHeight) - 1)\n\n  // First-load skeleton\n  if (loading && data.length === 0) {\n    return (\n      <DataTableSkeleton\n        columns={columns}\n        rowCount={skeletonRowCount}\n        hasTitle={!!title}\n        hasSubtitle={!!subtitle}\n        hasFooter={!!footer}\n        showSearch={showSearch}\n        hasToolbar={!!(toolbar || showDownload)}\n        pagination={pagination}\n        rounded={rounded}\n        className={className}\n      />\n    )\n  }\n\n  const hasToolbarRow = !!(toolbar || showDownload || showSearch)\n\n  return (\n    <div\n      data-slot=\"data-table\"\n      className={cn(\"flex w-full flex-col gap-4\", className)}\n      {...props}\n    >\n      {/* ── Header ── */}\n      {(title || subtitle) && (\n        <div className=\"flex flex-col gap-0.5\">\n          {title && (\n            <h3 className=\"text-sm leading-tight font-semibold text-foreground\">\n              {title}\n            </h3>\n          )}\n          {subtitle && (\n            <p className=\"text-xs text-muted-foreground\">{subtitle}</p>\n          )}\n        </div>\n      )}\n\n      {/* ── Toolbar ── */}\n      {hasToolbarRow && (\n        <div className=\"flex items-center justify-between gap-4 px-4 py-2\">\n          <div className=\"flex items-center gap-2\">\n            {toolbar}\n            {showDownload && (\n              <Button variant=\"outline\" size=\"sm\" onClick={onDownload}>\n                <Download data-icon=\"inline-start\" />\n                {l.actions.download}\n              </Button>\n            )}\n          </div>\n\n          {showSearch && (\n            <SearchBar\n              value={globalFilter}\n              onChange={handleGlobalFilter}\n              placeholder={l.searchPlaceholder}\n              size=\"sm\"\n              rounded=\"md\"\n              disabled={loading}\n              locale={locale}\n              voice={voiceSearch}\n              onVoiceStart={onVoiceStart}\n              onVoiceEnd={onVoiceEnd}\n              onVoiceError={onVoiceError}\n              className=\"mt-2 mr-2 w-full max-w-full min-w-36 sm:w-72\"\n            />\n          )}\n        </div>\n      )}\n\n      {/* ── Table wrapper ── */}\n      <div\n        className={cn(\n          \"relative w-full overflow-hidden border border-border bg-card shadow-sm\",\n          rounded && \"rounded-xl\"\n        )}\n        style={{ width: width ?? \"100%\" }}\n      >\n        {/* Refetch progress bar */}\n        {loading && data.length > 0 && (\n          <div\n            className={cn(\n              \"absolute inset-x-0 top-0 z-50 h-0.5 overflow-hidden\",\n              rounded && \"rounded-t-xl\"\n            )}\n          >\n            <div className=\"h-full animate-[shimmer_1.4s_ease-in-out_infinite] bg-primary opacity-70\" />\n          </div>\n        )}\n\n        {/* Scrollable viewport */}\n        <div\n          ref={parentRef}\n          className=\"relative max-h-[80dvh] w-full overflow-auto bg-card focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n          style={{ height: `${height}px` }}\n          tabIndex={0}\n          role=\"region\"\n          aria-label={\n            ariaLabel ??\n            `${i18n?.dataTable.tableWithRows ?? \"Table with\"} ${data.length} ${l.pagination.pluralItemName}`\n          }\n          aria-busy={loading}\n        >\n          <Table className=\"block w-full\" style={{ minWidth: minTableWidth }}>\n            {/* ── Sticky header ── */}\n            <TableHeader\n              sticky\n              className=\"block w-full\"\n              style={{ minWidth: minTableWidth }}\n            >\n              {table.getHeaderGroups().map((headerGroup) => (\n                <TableRow key={headerGroup.id} className=\"flex w-full\">\n                  {headerGroup.headers.map((header, hi) => {\n                    const meta = header.column.columnDef.meta\n                    const isSelect = header.column.id === \"__select__\"\n                    const colIdx = isSelect ? -1 : hi - (selectRows ? 1 : 0)\n                    const isSticky = isSelect || colIdx < stickyColumns\n                    const stickyLeft = isSelect\n                      ? 0\n                      : (stickyLefts[colIdx] ?? undefined)\n                    const isSorted = header.column.getIsSorted()\n                    const canSort = header.column.getCanSort()\n\n                    const sizedWidth = table.state.columnSizing[header.id]\n\n                    return (\n                      <TableHead\n                        key={header.id}\n                        className={cn(\n                          \"relative flex min-h-[44px] items-center gap-1.5 whitespace-nowrap select-none\",\n                          pad,\n                          isSelect && \"w-12 shrink-0 justify-center px-0\",\n                          meta?.align === \"center\" &&\n                            \"justify-center text-center\",\n                          meta?.align === \"right\" && \"justify-end text-right\",\n                          canSort &&\n                            \"cursor-pointer hover:bg-primary/10 hover:text-foreground\",\n                          isSticky &&\n                            !isSelect &&\n                            \"sticky z-20 bg-muted text-foreground\",\n                          isSelect && \"sticky z-30 bg-muted\",\n                          isSorted &&\n                            \"border-b-2 border-primary bg-primary/15 text-primary\"\n                        )}\n                        style={{\n                          ...(isSelect\n                            ? { width: 48, flexGrow: 0, flexShrink: 0 }\n                            : colStyle(meta, sizedWidth)),\n                          left: stickyLeft,\n                        }}\n                        onClick={header.column.getToggleSortingHandler()}\n                      >\n                        {isSelect ? (\n                          header.isPlaceholder ? null : (\n                            flexRender(\n                              header.column.columnDef.header,\n                              header.getContext()\n                            )\n                          )\n                        ) : (\n                          <span className=\"min-w-0 flex-1 truncate\">\n                            {header.isPlaceholder\n                              ? null\n                              : flexRender(\n                                  header.column.columnDef.header,\n                                  header.getContext()\n                                )}\n                          </span>\n                        )}\n                        {canSort && (\n                          <span\n                            className={cn(\n                              meta?.align === \"right\" ||\n                                meta?.align === \"center\"\n                                ? \"shrink-0\"\n                                : \"ml-auto shrink-0\",\n                              isSorted ? \"opacity-100\" : \"opacity-60\"\n                            )}\n                          >\n                            {isSorted === \"asc\" ? (\n                              <ArrowUp className=\"size-3\" />\n                            ) : isSorted === \"desc\" ? (\n                              <ArrowDown className=\"size-3\" />\n                            ) : (\n                              <ChevronsUpDown className=\"size-3\" />\n                            )}\n                          </span>\n                        )}\n                        {!isSelect && !!meta?.width && (\n                          <div\n                            onMouseDown={(e) => {\n                              e.stopPropagation()\n                              header.getResizeHandler()(e)\n                            }}\n                            onTouchStart={header.getResizeHandler()}\n                            className=\"group/resizer absolute top-0 right-0 flex h-full w-2 cursor-col-resize touch-none items-center justify-end select-none\"\n                          >\n                            <span\n                              className={cn(\n                                \"block h-4/5 w-px transition-colors\",\n                                header.column.getIsResizing()\n                                  ? \"bg-primary\"\n                                  : \"bg-border group-hover/resizer:bg-primary/50\"\n                              )}\n                            />\n                          </div>\n                        )}\n                      </TableHead>\n                    )\n                  })}\n                </TableRow>\n              ))}\n            </TableHeader>\n\n            {/* ── Virtualized body ── */}\n            <TableBody\n              style={{\n                display: \"block\",\n                height: `${virtualizer.getTotalSize()}px`,\n                position: \"relative\",\n                width: \"100%\",\n                minWidth: minTableWidth,\n              }}\n            >\n              {/* Data rows */}\n              {virtualItems.map((vRow) => {\n                const row = rows[vRow.index]\n                const isSelected = row.getIsSelected()\n\n                return (\n                  <TableRow\n                    key={row.id}\n                    data-selected={isSelected || undefined}\n                    clickable={!!onRowClick}\n                    onClick={() => onRowClick?.(row.original)}\n                    className={cn(\n                      \"group absolute top-0 left-0 flex w-full\",\n                      isSelected && \"bg-primary/5\",\n                      loading && \"pointer-events-none opacity-50\"\n                    )}\n                    style={{\n                      height: `${vRow.size}px`,\n                      transform: `translateY(${vRow.start}px)`,\n                      minWidth: minTableWidth,\n                    }}\n                  >\n                    {row.getAllCells().map((cell, ci) => {\n                      const meta = cell.column.columnDef.meta\n                      const isSelect = cell.column.id === \"__select__\"\n                      const colIdx = isSelect ? -1 : ci - (selectRows ? 1 : 0)\n                      const isSticky = isSelect || colIdx < stickyColumns\n                      const stickyLeft = isSelect\n                        ? 0\n                        : (stickyLefts[colIdx] ?? undefined)\n\n                      const isCellSorted =\n                        !isSelect && cell.column.getIsSorted() !== false\n\n                      return (\n                        <TableCell\n                          key={cell.id}\n                          className={cn(\n                            \"flex items-center truncate\",\n                            pad,\n                            font,\n                            isSelect && \"w-12 shrink-0 justify-center px-0\",\n                            meta?.align === \"center\" &&\n                              \"justify-center text-center\",\n                            meta?.align === \"right\" && \"justify-end text-right\",\n                            isCellSorted && !isSticky && \"bg-muted/30\",\n                            isSticky &&\n                              !isSelect &&\n                              \"sticky z-10 bg-card group-hover:bg-muted group-data-selected:bg-primary/5\",\n                            isSelect &&\n                              \"sticky z-20 bg-card group-hover:bg-muted group-data-selected:bg-primary/10\",\n                            meta?.wrap &&\n                              \"h-auto items-start py-3 leading-relaxed wrap-break-word whitespace-normal\"\n                          )}\n                          style={{\n                            ...(isSelect\n                              ? { width: 48, flexGrow: 0, flexShrink: 0 }\n                              : colStyle(\n                                  meta,\n                                  table.state.columnSizing[cell.column.id]\n                                )),\n                            left: stickyLeft,\n                          }}\n                        >\n                          {flexRender(\n                            cell.column.columnDef.cell,\n                            cell.getContext()\n                          )}\n                        </TableCell>\n                      )\n                    })}\n                  </TableRow>\n                )\n              })}\n            </TableBody>\n          </Table>\n\n          {/* Empty state — outside TableBody so inset-0 is relative to the fixed-height viewport */}\n          {!loading && rows.length === 0 && (\n            <div className=\"absolute inset-0 flex items-center justify-center\">\n              <DataTableEmpty\n                message={l.noData}\n                description={l.noDataDescription}\n              />\n            </div>\n          )}\n        </div>\n\n        {/* ── Pagination bar ── */}\n        {pagination && (\n          <PaginationBar\n            pageIndex={table.state.pagination.pageIndex}\n            pageCount={table.getPageCount()}\n            totalRows={\n              manualPagination && rowCount != null\n                ? rowCount\n                : table.getFilteredRowModel().rows.length\n            }\n            pageSize={table.state.pagination.pageSize}\n            pageSizeOptions={pageSizeOptions}\n            canPrev={table.getCanPreviousPage()}\n            canNext={table.getCanNextPage()}\n            onPrev={() => table.previousPage()}\n            onNext={() => table.nextPage()}\n            onPageChange={(i) => table.setPageIndex(i)}\n            onPageSizeChange={\n              pageSizeOptions ? (s) => table.setPageSize(s) : undefined\n            }\n            itemLabel={l.pagination.pluralItemName}\n            rowsPerPageLabel={l.pagination.rowsPerPage}\n            ofLabel={l.pagination.of}\n            prevLabel={l.pagination.prev}\n            nextLabel={l.pagination.next}\n            rounded={paginationRounded}\n          />\n        )}\n      </div>\n\n      {/* ── Footer ── */}\n      {footer && (\n        <div className=\"flex items-center gap-2 px-1 text-xs text-muted-foreground\">\n          {footer}\n        </div>\n      )}\n    </div>\n  )\n}\n\nDataTable.displayName = \"DataTable\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
