{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/data-table/DataTable.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport type {\n  ColumnDef,\n  ColumnFiltersState,\n  ColumnPinningState,\n  FilterFn,\n  SortingState,\n  VisibilityState,\n  ExpandedState,\n  GroupingState,\n  Table as TanstackTable,\n  Row,\n} from '@tanstack/react-table'\nimport {\n  flexRender,\n  getCoreRowModel,\n  getExpandedRowModel,\n  getFilteredRowModel,\n  getGroupedRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n} from '@tanstack/react-table'\nimport type { DateRange } from 'react-day-picker'\n// `valueUpdater` lives next to the low-level table primitive (which\n// data-table depends on transitively via registryDependencies). Keep\n// `@/lib/utils` reserved for the cn() helper shipped by the init\n// bootstrap -- importing valueUpdater from there would force every\n// consumer to hand-edit lib/utils.ts on install.\nimport { valueUpdater } from '@/components/ui/table/utils'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'\n\nimport { DataTableToolbar } from './DataTableToolbar'\nimport { DataTableFilterSheet } from './DataTableFilterSheet'\nimport { DataTablePagination } from './DataTablePagination'\nimport { type FilterDefinition, type FilterOption, resolveOption } from './types'\nimport { isoToDate, dateToIso } from './date-utils'\n\nexport type { FilterDefinition, FilterOption }\n\ninterface DateRangeValue {\n  from?: string\n  to?: string\n}\n\nexport interface DataTableState {\n  page: number\n  pageSize: number\n  sortBy: string\n  sortOrder: 'asc' | 'desc'\n  filters: Record<string, any>\n  search: string\n}\n\nexport interface DataTableProps<TData, TValue> {\n  columns: ColumnDef<TData, TValue>[]\n  data: TData[]\n  filterColumn?: string\n  filterPlaceholder?: string\n  filters?: FilterDefinition[]\n  filterMode?: 'inline' | 'modal' | 'popover'\n  /** Show the global search input. Default true. */\n  enableSearch?: boolean\n  /** Show the View column-visibility dropdown. Default false. */\n  enableColumnVisibility?: boolean\n  /** Show the pagination footer. Default true. */\n  enablePagination?: boolean\n  /** Hide the entire toolbar (search + filters + view). Default false. */\n  hideToolbar?: boolean\n  /** Show Export-CSV button in toolbar. Default false. */\n  enableExport?: boolean\n  /** Infinite-scroll mode: calls `onFetchMore` when last row enters viewport,\n   *  hides the pagination footer. Combine with append-on-success on the\n   *  consumer side. */\n  infinite?: boolean\n  /** Allow drag-to-resize on column borders. */\n  enableResize?: boolean\n  /** Initial column pinning. Each column id is pinned to the given side. */\n  defaultColumnPinning?: { left?: string[]; right?: string[] }\n  /** Allow drag-to-reorder on column headers. */\n  enableReorder?: boolean\n  /** Initial column ids to group by. Pass an empty array to disable grouping. */\n  defaultGrouping?: string[]\n  /** Virtual-scrolling mode (CSS content-visibility based). Best with large\n   *  datasets and `maxHeight` for a scroll container. */\n  virtual?: boolean\n  /** Sticky header — keeps `<thead>` visible when scrolling. Pair with `maxHeight`. */\n  stickyHeader?: boolean\n  /** Density of cell padding: 'compact' | 'cozy' | 'comfortable'. Default cozy.\n   *  Treated as the INITIAL density when `enableDensityToggle` is on; the user\n   *  can override it at runtime from the toolbar. */\n  density?: 'compact' | 'cozy' | 'comfortable'\n  /** Show the density toggle in the toolbar. Default false. */\n  enableDensityToggle?: boolean\n  /** Strip the DataTable's borders.\n   *  - `'inner'` removes toolbar bottom-divider, pagination top-divider,\n   *    and filter-sheet section bgs/borders. The outer container border\n   *    is kept.\n   *  - `'full'` additionally removes the outer container border + rounded\n   *    corners so the table renders completely flat on the canvas. */\n  borderless?: 'inner' | 'full'\n  /** Where the toolbar renders. `'inside'` (default) lives inside the\n   *  bordered container; `'above'` floats outside it. */\n  toolbarPosition?: 'inside' | 'above'\n  /** Where the pagination footer renders. `'inside'` (default) or `'below'`. */\n  paginationPosition?: 'inside' | 'below'\n  /** Max height; enables vertical scroll inside the card. */\n  maxHeight?: string\n  /** Row click — when set, rows become clickable + cursor-pointer. */\n  onRowClick?: (row: TData) => void\n  /** Server-side mode: total row count from API (enables manual pagination) */\n  totalRows?: number\n  /** Server-side mode: loading state indicator */\n  loading?: boolean\n  /** Emitted when server-side state changes (pagination, sorting, filters) */\n  onStateChange?: (state: DataTableState) => void\n  /** Infinite-scroll: last row entered viewport, time to load more */\n  onFetchMore?: () => void\n  /** Custom empty-state content (replaces \"No results.\"). */\n  emptyState?: React.ReactNode\n  /** Expanded-row content. Receives the original row + the TanStack row. */\n  renderExpanded?: (row: TData, tanstackRow: Row<TData>) => React.ReactNode\n  /** Bulk action bar content (shown above the table when rows are selected). */\n  renderBulkActions?: (rows: Row<TData>[], clear: () => void) => React.ReactNode\n  /** Footer (<tfoot>) content. */\n  renderFooter?: (rows: Row<TData>[]) => React.ReactNode\n  /** Extra toolbar controls (e.g. group-by selector). */\n  toolbarExtra?: React.ReactNode\n  /** Consumer-supplied custom filter UI inside the filter surface. */\n  customFilters?: React.ReactNode\n}\n\nexport interface DataTableHandle<TData> {\n  table: TanstackTable<TData>\n  exportCsv: () => void\n  exportJson: () => void\n}\n\nfunction DataTableInner<TData, TValue>(\n  props: DataTableProps<TData, TValue>,\n  forwardedRef: React.Ref<DataTableHandle<TData>>,\n) {\n  const {\n    columns,\n    data,\n    filterColumn = '',\n    filterPlaceholder = 'Filter...',\n    filters = [],\n    filterMode = 'modal',\n    enableSearch = true,\n    enableColumnVisibility = false,\n    enablePagination = true,\n    hideToolbar = false,\n    enableExport = false,\n    infinite = false,\n    enableResize = false,\n    defaultColumnPinning = { left: [], right: [] },\n    enableReorder = false,\n    defaultGrouping = [],\n    virtual = false,\n    stickyHeader = false,\n    density = 'cozy',\n    enableDensityToggle = false,\n    borderless,\n    toolbarPosition = 'inside',\n    paginationPosition = 'inside',\n    maxHeight = '',\n    onRowClick,\n    totalRows = -1,\n    loading = false,\n    onStateChange,\n    onFetchMore,\n    emptyState,\n    renderExpanded,\n    renderBulkActions,\n    renderFooter,\n    toolbarExtra,\n    customFilters,\n  } = props\n\n  // User-mutable density -- initial value from prop, toggleable via toolbar\n  // when `enableDensityToggle` is on. Sync the prop so parents can still\n  // drive it externally.\n  const [currentDensity, setCurrentDensity] = React.useState<'compact' | 'cozy' | 'comfortable'>(density)\n  React.useEffect(() => {\n    setCurrentDensity(density)\n  }, [density])\n\n  // `borderless` is an enum (`'inner'` vs `'full'`); children only need a\n  // boolean \"should I drop my borders?\".\n  const dropInnerBorders = !!borderless\n\n  const densityClass =\n    currentDensity === 'compact'\n      ? '[&_td]:py-1.5 [&_td]:px-3 [&_td]:text-xs [&_th]:h-8 [&_th]:px-3 [&_th]:text-xs'\n      : currentDensity === 'comfortable'\n        ? '[&_td]:py-3 [&_th]:h-12'\n        : // cozy -- tightens TableCell/TableHead defaults (py-3 / h-12)\n          '[&_td]:py-2 [&_th]:h-10'\n\n  const isServerSide = totalRows >= 0\n  const [isFilterSheetOpen, setIsFilterSheetOpen] = React.useState(false)\n\n  // ── Filter snapshot (restore on close without apply) ───────────────────\n  const filterSnapshot = React.useRef<ColumnFiltersState | null>(null)\n  const dateRangeSnapshot = React.useRef<Record<string, DateRangeValue> | null>(null)\n  const filterApplied = React.useRef(false)\n\n  // ── Timers ──────────────────────────────────────────────────────────────\n  const searchDebounceTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n  const paginationEmitTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  // Custom filter functions for multiselect and date range\n  const multiSelectFilterFn: FilterFn<TData> = (row, columnId, filterValue: string[]) => {\n    if (!filterValue || filterValue.length === 0) return true\n    const cellValue = String(row.getValue(columnId)).toLowerCase()\n    return filterValue.some((v: string) => v.toLowerCase() === cellValue)\n  }\n\n  const dateRangeFilterFn: FilterFn<TData> = (row, columnId, filterValue: DateRangeValue) => {\n    if (!filterValue) return true\n    const { from, to } = filterValue\n    if (!from && !to) return true\n    const cellValue = String(row.getValue(columnId))\n    if (from && cellValue < from) return false\n    if (to && cellValue > to) return false\n    return true\n  }\n\n  // Augment columns with custom filter functions based on filter definitions\n  const processedColumns = React.useMemo(() => {\n    return columns.map((col) => {\n      const colId = (col as any).accessorKey || (col as any).id\n      const filter = filters.find((f) => f.column === colId)\n      if (!filter) return col\n      if (filter.type === 'multiselect') return { ...col, filterFn: multiSelectFilterFn }\n      if (filter.type === 'date') return { ...col, filterFn: dateRangeFilterFn }\n      return col\n    })\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [columns, filters])\n\n  const [sorting, setSorting] = React.useState<SortingState>([])\n  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])\n  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})\n  const [rowSelection, setRowSelection] = React.useState({})\n  const [expanded, setExpanded] = React.useState<ExpandedState>({})\n  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({\n    left: defaultColumnPinning.left ?? [],\n    right: defaultColumnPinning.right ?? [],\n  })\n  const [columnOrder, setColumnOrder] = React.useState<string[]>([])\n  const [grouping, setGrouping] = React.useState<GroupingState>(defaultGrouping)\n  const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10 })\n\n  // Per-filter reactive range calendar model (ISO strings), keyed by column.\n  const [dateRangeModels, setDateRangeModels] = React.useState<Record<string, DateRangeValue>>({})\n\n  const table = useReactTable({\n    data,\n    columns: processedColumns,\n    getCoreRowModel: getCoreRowModel(),\n    getPaginationRowModel: isServerSide ? undefined : getPaginationRowModel(),\n    getSortedRowModel: isServerSide ? undefined : getSortedRowModel(),\n    getFilteredRowModel: isServerSide ? undefined : getFilteredRowModel(),\n    getExpandedRowModel: getExpandedRowModel(),\n    getGroupedRowModel: getGroupedRowModel(),\n    enableColumnResizing: enableResize,\n    columnResizeMode: 'onChange',\n    manualPagination: isServerSide,\n    manualSorting: isServerSide,\n    manualFiltering: isServerSide,\n    rowCount: isServerSide ? totalRows : undefined,\n    onSortingChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, setSorting)\n      if (isServerSide) queueMicrotask(emitStateUpdate)\n    },\n    onColumnFiltersChange: (updaterOrValue) => {\n      // Server-side: never auto-emit on filter change — handled by Apply / search debounce\n      // Client-side inline: filters apply locally via TanStack, no emit needed\n      valueUpdater(updaterOrValue, setColumnFilters)\n    },\n    onColumnVisibilityChange: (updaterOrValue) => valueUpdater(updaterOrValue, setColumnVisibility),\n    onRowSelectionChange: (updaterOrValue) => valueUpdater(updaterOrValue, setRowSelection),\n    onExpandedChange: (updaterOrValue) => valueUpdater(updaterOrValue, setExpanded),\n    onPaginationChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, setPagination)\n      if (isServerSide) {\n        if (paginationEmitTimer.current) clearTimeout(paginationEmitTimer.current)\n        paginationEmitTimer.current = setTimeout(() => emitStateUpdate(), 0)\n      }\n    },\n    onColumnPinningChange: (updaterOrValue) => valueUpdater(updaterOrValue, setColumnPinning),\n    onColumnOrderChange: (updaterOrValue) => valueUpdater(updaterOrValue, setColumnOrder),\n    onGroupingChange: (updaterOrValue) => valueUpdater(updaterOrValue, setGrouping),\n    state: {\n      sorting,\n      columnFilters,\n      columnVisibility,\n      pagination,\n      rowSelection,\n      expanded,\n      columnPinning,\n      columnOrder,\n      grouping,\n    },\n  })\n\n  /** Build and emit current server-side state */\n  function emitStateUpdate() {\n    const s = sorting[0]\n    const filterMap: Record<string, any> = {}\n    for (const cf of columnFilters) {\n      filterMap[cf.id] = cf.value\n    }\n    onStateChange?.({\n      page: table.getState().pagination.pageIndex + 1,\n      pageSize: table.getState().pagination.pageSize,\n      sortBy: s?.id || '',\n      sortOrder: s?.desc ? 'desc' : 'asc',\n      filters: filterMap,\n      search: (filterColumn && (table.getColumn(filterColumn)?.getFilterValue() as string)) || '',\n    })\n  }\n\n  function onSearchInput(val: string) {\n    if (!filterColumn || !table.getColumn(filterColumn)) return\n    table.getColumn(filterColumn)?.setFilterValue(val || undefined)\n    if (isServerSide) {\n      if (searchDebounceTimer.current) clearTimeout(searchDebounceTimer.current)\n      searchDebounceTimer.current = setTimeout(() => {\n        table.setPageIndex(0)\n        emitStateUpdate()\n      }, 500)\n    }\n  }\n\n  // ── Filter snapshot helpers ──────────────────────────────────────────────\n  function snapshotFilters() {\n    filterApplied.current = false\n    filterSnapshot.current = JSON.parse(JSON.stringify(columnFilters))\n    dateRangeSnapshot.current = JSON.parse(JSON.stringify(dateRangeModels))\n  }\n\n  function maybeRestoreFilters() {\n    if (!filterApplied.current && filterSnapshot.current) {\n      setColumnFilters(filterSnapshot.current)\n      setDateRangeModels(dateRangeSnapshot.current ?? {})\n    }\n    filterSnapshot.current = null\n    dateRangeSnapshot.current = null\n  }\n\n  function onFilterSheetOpen() {\n    snapshotFilters()\n    setIsFilterSheetOpen(true)\n  }\n\n  function onFilterSheetClose(open: boolean) {\n    if (!open) maybeRestoreFilters()\n    setIsFilterSheetOpen(open)\n  }\n\n  // ── Filter helpers ─────────────────────────────────────────────────────\n  function getMultiSelectValue(column: string): string[] {\n    return (table.getColumn(column)?.getFilterValue() as string[]) ?? []\n  }\n\n  function getDateRangeValue(column: string): DateRangeValue {\n    return (table.getColumn(column)?.getFilterValue() as DateRangeValue) ?? {}\n  }\n\n  function getCalendarModel(column: string): DateRange | undefined {\n    const dr = getDateRangeValue(column)\n    if (!dr.from && !dr.to) return undefined\n    return { from: isoToDate(dr.from), to: isoToDate(dr.to) }\n  }\n\n  function onCalendarUpdate(column: string, val: DateRange | undefined) {\n    const from = val?.from ? dateToIso(val.from) : undefined\n    const to = val?.to ? dateToIso(val.to) : undefined\n    setDateRangeModels((m) => ({ ...m, [column]: { from, to } }))\n    const hasValue = from || to\n    table.getColumn(column)?.setFilterValue(hasValue ? { from, to } : undefined)\n  }\n\n  function formatDateRange(column: string): string {\n    const dr = getDateRangeValue(column)\n    if (dr.from && dr.to) return `${dr.from} - ${dr.to}`\n    if (dr.from) return `From ${dr.from}`\n    if (dr.to) return `Until ${dr.to}`\n    return ''\n  }\n\n  function toggleMultiSelectValue(column: string, option: string) {\n    const current = getMultiSelectValue(column)\n    const next = current.includes(option) ? current.filter((v) => v !== option) : [...current, option]\n    table.getColumn(column)?.setFilterValue(next.length > 0 ? next : undefined)\n  }\n\n  function clearAllFilters() {\n    filterApplied.current = true\n    if (searchDebounceTimer.current) clearTimeout(searchDebounceTimer.current)\n    filters.forEach((f) => {\n      table.getColumn(f.column)?.setFilterValue(undefined)\n      if (f.type === 'date') {\n        setDateRangeModels((m) => ({ ...m, [f.column]: {} }))\n      }\n    })\n    if (filterColumn && table.getColumn(filterColumn)) {\n      table.getColumn(filterColumn)?.setFilterValue(undefined)\n    }\n    if (isServerSide) {\n      table.setPageIndex(0)\n      queueMicrotask(() => emitStateUpdate())\n    }\n  }\n\n  function isFilterActive(filter: FilterDefinition): boolean {\n    const value = table.getColumn(filter.column)?.getFilterValue()\n    if (value === undefined || value === '') return false\n    if (filter.type === 'multiselect') return Array.isArray(value) && value.length > 0\n    if (filter.type === 'date') {\n      const d = value as DateRangeValue\n      return !!(d.from || d.to)\n    }\n    return true\n  }\n\n  const hasSearchValue = !!(filterColumn && (table.getColumn(filterColumn)?.getFilterValue() as string))\n  const isAnyFilterActive = hasSearchValue || filters.some(isFilterActive)\n\n  function getFilterSelectedLabels(filter: FilterDefinition): string[] {\n    const vals = getMultiSelectValue(filter.column)\n    return vals.map((v) => {\n      const opt = filter.options?.find((o) => resolveOption(o).value === v)\n      return opt ? resolveOption(opt).label : v\n    })\n  }\n\n  function clearFilter(filter: FilterDefinition) {\n    table.getColumn(filter.column)?.setFilterValue(undefined)\n  }\n\n  const activeFilterCount = filters.filter(isFilterActive).length\n\n  function clearDateFilter(filter: FilterDefinition) {\n    clearFilter(filter)\n    setDateRangeModels((m) => ({ ...m, [filter.column]: {} }))\n  }\n\n  function onApplyFilters() {\n    filterApplied.current = true\n    if (isServerSide) {\n      if (table.getState().pagination.pageIndex === 0) {\n        emitStateUpdate()\n      } else {\n        table.setPageIndex(0)\n      }\n    }\n    setIsFilterSheetOpen(false)\n  }\n\n  /**\n   * Popover filter mode: handle the staged-edit commit. Walk the draft,\n   * write each column's value via `setFilterValue`, and sync the calendar\n   * model for date filters so a subsequent reopen reflects the just-applied\n   * range.\n   */\n  function onCommitDraft(draft: Record<string, any>) {\n    for (const f of filters) {\n      if (!(f.column in draft)) continue\n      const val = draft[f.column]\n      if (f.type === 'date') {\n        const dr = (val ?? {}) as DateRangeValue\n        setDateRangeModels((m) => ({ ...m, [f.column]: { from: dr.from, to: dr.to } }))\n        table.getColumn(f.column)?.setFilterValue(val)\n      } else {\n        table.getColumn(f.column)?.setFilterValue(val)\n      }\n    }\n    onApplyFilters()\n  }\n\n  // ── Column reorder (HTML5 drag/drop swaps dragged column with drop target) ──\n  const [dragColId, setDragColId] = React.useState<string | null>(null)\n  const [dragOverColId, setDragOverColId] = React.useState<string | null>(null)\n\n  function onColDragStart(id: string, e: React.DragEvent) {\n    setDragColId(id)\n    document.body.style.cursor = 'grabbing'\n    if (e.dataTransfer) {\n      e.dataTransfer.effectAllowed = 'move'\n      e.dataTransfer.setData('text/plain', id)\n    }\n  }\n\n  function onColDragOver(targetId: string, e: React.DragEvent) {\n    if (!enableReorder || !dragColId) return\n    e.preventDefault()\n    setDragOverColId(targetId)\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n  }\n\n  function onColDragEnd() {\n    document.body.style.cursor = ''\n    setDragColId(null)\n    setDragOverColId(null)\n  }\n\n  function onColDrop(targetId: string) {\n    const src = dragColId\n    onColDragEnd()\n    if (!src || src === targetId) return\n    const order = (columnOrder.length ? columnOrder : table.getAllLeafColumns().map((c) => c.id)).slice()\n    const from = order.indexOf(src)\n    const to = order.indexOf(targetId)\n    if (from === -1 || to === -1) return\n    order.splice(to, 0, ...order.splice(from, 1))\n    setColumnOrder(order)\n  }\n\n  // ── Pinning — return style for sticky pinned cells (header or body) ──\n  function pinStyle(col: any): React.CSSProperties | undefined {\n    const side = col.getIsPinned()\n    if (!side) return undefined\n    if (side === 'left') return { position: 'sticky', left: `${col.getStart('left')}px`, zIndex: 2 }\n    return { position: 'sticky', right: `${col.getAfter('right')}px`, zIndex: 2 }\n  }\n\n  // ── Infinite scroll — sentinel row triggers fetch-more when visible ──\n  const sentinelEl = React.useRef<HTMLDivElement | null>(null)\n  React.useEffect(() => {\n    if (!infinite || !sentinelEl.current) return\n    const io = new IntersectionObserver(\n      (entries) => {\n        if (entries[0]?.isIntersecting) onFetchMore?.()\n      },\n      { rootMargin: '100px' },\n    )\n    io.observe(sentinelEl.current)\n    return () => io.disconnect()\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [infinite])\n\n  // ── CSV export — currently filtered + visible data, skips select/actions/expander cols. ──\n  function exportCsv() {\n    const visibleCols = table\n      .getVisibleLeafColumns()\n      .filter((c) => c.id !== 'select' && c.id !== 'actions' && c.id !== 'expander')\n    const headers = visibleCols.map((c) =>\n      String(c.columnDef.header && typeof c.columnDef.header === 'string' ? c.columnDef.header : c.id),\n    )\n    const rows = table.getFilteredRowModel().rows.map((row) =>\n      visibleCols.map((c) => {\n        const v = row.getValue(c.id)\n        const s = v == null ? '' : String(v)\n        return /[\",\\n]/.test(s) ? `\"${s.replace(/\"/g, '\"\"')}\"` : s\n      }),\n    )\n    const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\\n')\n    downloadBlob(csv, 'text/csv;charset=utf-8;', `export-${Date.now()}.csv`)\n  }\n\n  // ── JSON export — same scope as CSV, but emits underlying row.original objects ──\n  function exportJson() {\n    const visibleColIds = table\n      .getVisibleLeafColumns()\n      .filter((c) => c.id !== 'select' && c.id !== 'actions' && c.id !== 'expander')\n      .map((c) => c.id)\n    const rows = table.getFilteredRowModel().rows.map((row) => {\n      const original = row.original as Record<string, unknown>\n      const out: Record<string, unknown> = {}\n      for (const id of visibleColIds) {\n        out[id] = id in original ? original[id] : row.getValue(id)\n      }\n      return out\n    })\n    const json = JSON.stringify(rows, null, 2)\n    downloadBlob(json, 'application/json;charset=utf-8;', `export-${Date.now()}.json`)\n  }\n\n  function downloadBlob(content: string, type: string, filename: string) {\n    const blob = new Blob([content], { type })\n    const url = URL.createObjectURL(blob)\n    const a = document.createElement('a')\n    a.href = url\n    a.download = filename\n    document.body.appendChild(a)\n    a.click()\n    document.body.removeChild(a)\n    URL.revokeObjectURL(url)\n  }\n\n  React.useImperativeHandle(forwardedRef, () => ({ table, exportCsv, exportJson }))\n\n  const selectedRows = table.getSelectedRowModel().rows\n\n  const toolbarProps = {\n    table: table as TanstackTable<any>,\n    filterColumn,\n    filterPlaceholder,\n    filters,\n    filterMode,\n    enableSearch,\n    enableColumnVisibility,\n    enableExport,\n    enableDensityToggle,\n    density: currentDensity,\n    activeFilterCount,\n    isAnyFilterActive,\n    isServerSide,\n    getMultiSelectValue,\n    getDateRangeValue,\n    getFilterSelectedLabels,\n    formatDateRange,\n    getCalendarModel,\n    onSearch: onSearchInput,\n    onOpenFilterSheet: onFilterSheetOpen,\n    onApplyFilters,\n    onClearAllFilters: clearAllFilters,\n    onToggleMultiselect: toggleMultiSelectValue,\n    onClearFilter: clearFilter,\n    onClearDateFilter: clearDateFilter,\n    onCalendarUpdate,\n    onTextFilterUpdate: (col: string, val: string | undefined) => table.getColumn(col)?.setFilterValue(val),\n    onCommitFilters: onCommitDraft,\n    onExportCsv: exportCsv,\n    onExportJson: exportJson,\n    onDensityChange: setCurrentDensity,\n    toolbarExtra,\n    customFilters,\n  }\n\n  return (\n    <div data-uipkge=\"\" data-slot=\"data-table\" className=\"w-full\">\n      {/* Toolbar (above-mode: floats outside the bordered card). */}\n      {!hideToolbar && toolbarPosition === 'above' && <DataTableToolbar {...toolbarProps} borderless />}\n\n      <div\n        className={[\n          'bg-card text-card-foreground overflow-hidden',\n          borderless === 'full' ? '' : 'rounded-md border',\n        ].join(' ')}\n      >\n        {/* Toolbar (inside-mode, default). */}\n        {!hideToolbar && toolbarPosition === 'inside' && (\n          <DataTableToolbar {...toolbarProps} borderless={dropInnerBorders} />\n        )}\n\n        {/* Filter Sheet (modal mode) */}\n        <DataTableFilterSheet\n          open={isFilterSheetOpen}\n          onOpenChange={onFilterSheetClose}\n          table={table as TanstackTable<any>}\n          filters={filters}\n          activeFilterCount={activeFilterCount}\n          isAnyFilterActive={isAnyFilterActive}\n          isServerSide={isServerSide}\n          borderless={dropInnerBorders}\n          getMultiSelectValue={getMultiSelectValue}\n          getDateRangeValue={getDateRangeValue}\n          formatDateRange={formatDateRange}\n          getCalendarModel={getCalendarModel}\n          onApply={onApplyFilters}\n          onClearAll={clearAllFilters}\n          onToggleMultiselect={toggleMultiSelectValue}\n          onClearFilter={clearFilter}\n          onClearDateFilter={clearDateFilter}\n          onCalendarUpdate={onCalendarUpdate}\n          onTextFilterUpdate={(col, val) => table.getColumn(col)?.setFilterValue(val)}\n          customFilters={customFilters}\n        />\n\n        {/* Bulk action bar (shown above table when rows selected) */}\n        {renderBulkActions && selectedRows.length > 0 && (\n          <div className=\"bg-muted/40 flex items-center gap-3 border-b px-4 py-2\">\n            <span className=\"text-sm font-medium\"> {selectedRows.length} selected </span>\n            {renderBulkActions(selectedRows, () => table.toggleAllRowsSelected(false))}\n            <button\n              type=\"button\"\n              className=\"text-muted-foreground hover:text-foreground ml-auto text-xs transition\"\n              onClick={() => table.toggleAllRowsSelected(false)}\n            >\n              Clear selection\n            </button>\n          </div>\n        )}\n\n        {/* Table. The Table primitive wraps the <table> in its own overflow-auto\n            div; neutralize that inner overflow and let the outer scroll\n            container own the scroll region. */}\n        <div\n          className={[densityClass, 'relative [&_[data-slot=table-container]]:overflow-visible', maxHeight ? 'overflow-auto' : ''].join(' ')}\n          style={maxHeight ? { maxHeight } : undefined}\n        >\n          {/* Loading overlay */}\n          {loading && (\n            <div className=\"bg-card/60 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-[1px]\">\n              <div className=\"border-primary size-5 animate-spin rounded-full border-2 border-t-transparent\" />\n            </div>\n          )}\n          <Table>\n            <TableHeader className={stickyHeader ? 'bg-muted/95 sticky top-0 z-10 backdrop-blur-sm' : ''}>\n              {table.getHeaderGroups().map((headerGroup) => (\n                <TableRow key={headerGroup.id}>\n                  {headerGroup.headers.map((header) => {\n                    const reorderable =\n                      enableReorder &&\n                      header.column.id !== 'select' &&\n                      header.column.id !== 'actions' &&\n                      header.column.id !== 'expander'\n                    return (\n                      <TableHead\n                        key={header.id}\n                        className={[\n                          'relative transition-colors duration-150',\n                          reorderable ? 'cursor-grab active:cursor-grabbing' : '',\n                          dragColId === header.column.id ? 'opacity-50' : '',\n                          dragOverColId === header.column.id && dragColId !== header.column.id\n                            ? 'border-foreground/60 border-l-2'\n                            : '',\n                        ].join(' ')}\n                        style={{\n                          ...(enableResize ? { width: `${header.getSize()}px` } : {}),\n                          ...(pinStyle(header.column) ?? {}),\n                        }}\n                        draggable={reorderable}\n                        onDragStart={reorderable ? (e) => onColDragStart(header.column.id, e) : undefined}\n                        onDragOver={reorderable ? (e) => onColDragOver(header.column.id, e) : undefined}\n                        onDragEnd={reorderable ? () => onColDragEnd() : undefined}\n                        onDrop={reorderable ? () => onColDrop(header.column.id) : undefined}\n                      >\n                        {!header.isPlaceholder &&\n                          flexRender(header.column.columnDef.header, header.getContext())}\n                        {enableResize && header.column.getCanResize() && (\n                          <div\n                            className={[\n                              'hover:bg-foreground/30 absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none transition-colors select-none',\n                              header.column.getIsResizing() ? 'bg-foreground/60' : '',\n                            ].join(' ')}\n                            onMouseDown={header.getResizeHandler()}\n                            onTouchStart={header.getResizeHandler()}\n                          />\n                        )}\n                      </TableHead>\n                    )\n                  })}\n                </TableRow>\n              ))}\n            </TableHeader>\n            <TableBody>\n              {table.getRowModel().rows?.length ? (\n                table.getRowModel().rows.map((row) => (\n                  <React.Fragment key={row.id}>\n                    <TableRow\n                      data-state={row.getIsSelected() ? 'selected' : undefined}\n                      className={onRowClick ? 'hover:bg-muted/40 cursor-pointer' : ''}\n                      style={\n                        virtual ? { contentVisibility: 'auto', containIntrinsicSize: 'auto 48px' } : undefined\n                      }\n                      onClick={() => onRowClick?.(row.original)}\n                    >\n                      {row.getVisibleCells().map((cell) => (\n                        <TableCell key={cell.id} className=\"bg-card\" style={pinStyle(cell.column)}>\n                          {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                        </TableCell>\n                      ))}\n                    </TableRow>\n                    {row.getIsExpanded() && renderExpanded && (\n                      <TableRow>\n                        <TableCell colSpan={row.getVisibleCells().length} className=\"bg-muted/30 px-6 py-4\">\n                          {renderExpanded(row.original, row)}\n                        </TableCell>\n                      </TableRow>\n                    )}\n                  </React.Fragment>\n                ))\n              ) : (\n                <TableRow>\n                  <TableCell colSpan={columns.length} className=\"h-24 text-center\">\n                    {emptyState ?? <div className=\"text-muted-foreground text-sm\">No results.</div>}\n                  </TableCell>\n                </TableRow>\n              )}\n            </TableBody>\n            {renderFooter && (\n              <tfoot className=\"bg-muted/20 sticky bottom-0 border-t\">{renderFooter(table.getRowModel().rows)}</tfoot>\n            )}\n          </Table>\n          {/* Infinite scroll sentinel */}\n          {infinite && <div ref={sentinelEl} className=\"h-1\" />}\n          {infinite && loading && (\n            <div className=\"border-border text-muted-foreground border-t px-4 py-3 text-center text-sm\">\n              Loading more…\n            </div>\n          )}\n        </div>\n\n        {/* Pagination (inside-mode, default) */}\n        {enablePagination && !infinite && paginationPosition === 'inside' && (\n          <DataTablePagination\n            table={table as TanstackTable<any>}\n            totalRows={totalRows}\n            isServerSide={isServerSide}\n            borderless={dropInnerBorders}\n          />\n        )}\n      </div>\n\n      {/* Pagination (below-mode: floats outside the bordered card). */}\n      {enablePagination && !infinite && paginationPosition === 'below' && (\n        <DataTablePagination\n          table={table as TanstackTable<any>}\n          totalRows={totalRows}\n          isServerSide={isServerSide}\n          borderless\n        />\n      )}\n    </div>\n  )\n}\n\n// forwardRef + generics: cast the wrapped component back to a generic-aware\n// signature so consumers keep `<DataTable<Row>>` inference.\nexport const DataTable = React.forwardRef(DataTableInner) as <TData, TValue = unknown>(\n  props: DataTableProps<TData, TValue> & { ref?: React.Ref<DataTableHandle<TData>> },\n) => React.ReactElement\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/DataTable.tsx"
    },
    {
      "path": "packages/registry-react/components/data-table/DataTableColumnHeader.tsx",
      "content": "'use client'\n\n/**\n * Sortable / hideable header cell for TanStack DataTable columns. Use it from\n * a column definition like:\n *   header: ({ column }) => <DataTableColumnHeader column={column} label=\"Email\" />\n *\n * Click on the label cycles sort asc → desc → none.\n *\n * Optional per-column filter:\n *   header: ({ column }) => (\n *     <DataTableColumnHeader\n *       column={column}\n *       label=\"Status\"\n *       filter={{ column: 'status', label: 'Status', type: 'multiselect', options: [...] }}\n *     />\n *   )\n *\n * When `filter` is provided, a funnel icon renders next to the sort button.\n * Click opens a popover with the right filter UI for the type (text / select\n * / multiselect / date). The funnel shows a primary-coloured dot when the\n * column has an active filter.\n */\nimport * as React from 'react'\nimport type { Column } from '@tanstack/react-table'\nimport { ArrowUp, ArrowUpDown, Check, Filter, FilterX } from 'lucide-react'\nimport type { DateRange } from 'react-day-picker'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { RangeCalendar } from '@/components/ui/range-calendar'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'\nimport { isoToDate, dateToIso } from './date-utils'\n\ninterface FilterOption {\n  value: string\n  label: string\n}\ninterface FilterDefinition {\n  column: string\n  label: string\n  type: 'text' | 'select' | 'multiselect' | 'date'\n  options?: (string | FilterOption)[]\n}\n\nexport interface DataTableColumnHeaderProps<TData, TValue> {\n  column: Column<TData, TValue>\n  label: string\n  align?: 'left' | 'right' | 'center'\n  filter?: FilterDefinition\n  className?: string\n}\n\nfunction resolveOption(opt: string | FilterOption): FilterOption {\n  return typeof opt === 'string' ? { value: opt, label: opt } : opt\n}\n\nexport function DataTableColumnHeader<TData, TValue>({\n  column,\n  label,\n  align = 'left',\n  filter,\n  className,\n}: DataTableColumnHeaderProps<TData, TValue>) {\n  const [open, setOpen] = React.useState(false)\n\n  function next() {\n    const current = column.getIsSorted()\n    if (!current) column.toggleSorting(false)\n    else if (current === 'asc') column.toggleSorting(true)\n    else column.clearSorting()\n  }\n\n  const filterValue = column.getFilterValue()\n\n  const isFilterActive = (() => {\n    const v = filterValue\n    if (v === undefined || v === null || v === '') return false\n    if (Array.isArray(v)) return v.length > 0\n    if (typeof v === 'object') return Object.keys(v).length > 0\n    return true\n  })()\n\n  // Text-input bound separately so we can apply on blur / Enter rather than\n  // thrashing the column filter on every keystroke.\n  const [textDraft, setTextDraft] = React.useState('')\n  React.useEffect(() => {\n    if (open && filter?.type === 'text') {\n      setTextDraft((filterValue as string) ?? '')\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open])\n\n  function applyText() {\n    column.setFilterValue(textDraft || undefined)\n  }\n\n  function clearText() {\n    setTextDraft('')\n    column.setFilterValue(undefined)\n  }\n\n  function toggleMultiselect(value: string) {\n    const current = (filterValue as string[]) ?? []\n    const nextVal = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]\n    column.setFilterValue(nextVal.length > 0 ? nextVal : undefined)\n  }\n\n  function selectOne(value: string) {\n    column.setFilterValue(value || undefined)\n    setOpen(false)\n  }\n\n  function clearFilter() {\n    column.setFilterValue(undefined)\n    setTextDraft('')\n  }\n\n  // Date-range bridging. The column stores ISO strings; the calendar wants\n  // native Date instances. Reads/writes both shapes.\n  const dateModel: DateRange | undefined = (() => {\n    const v = filterValue as { from?: string; to?: string } | undefined\n    if (!v?.from && !v?.to) return undefined\n    return {\n      from: v.from ? isoToDate(v.from) : undefined,\n      to: v.to ? isoToDate(v.to) : undefined,\n    }\n  })()\n\n  function onDateSelect(range: DateRange | undefined) {\n    const isoFrom = range?.from ? dateToIso(range.from) : undefined\n    const isoTo = range?.to ? dateToIso(range.to) : undefined\n    if (!isoFrom && !isoTo) {\n      column.setFilterValue(undefined)\n    } else {\n      column.setFilterValue({ from: isoFrom, to: isoTo })\n    }\n  }\n\n  function selectedLabels(): string[] {\n    if (!filter?.options) return []\n    const selected = (filterValue as string[]) ?? []\n    return filter.options\n      .map(resolveOption)\n      .filter((o) => selected.includes(o.value))\n      .map((o) => o.label)\n  }\n\n  return (\n    <div\n      className={cn(\n        'flex items-center gap-0.5',\n        align === 'right' && 'justify-end',\n        align === 'center' && 'justify-center',\n        className,\n      )}\n    >\n      {column.getCanSort() ? (\n        <button\n          type=\"button\"\n          className=\"group text-muted-foreground hover:text-foreground hover:bg-muted/60 -mx-2 inline-flex items-center gap-1.5 rounded px-2 py-1 text-sm font-medium transition-colors duration-150\"\n          aria-label={`Sort by ${label}`}\n          onClick={next}\n        >\n          <span>{label}</span>\n          {column.getIsSorted() ? (\n            <ArrowUp\n              className={cn(\n                'text-foreground size-3.5 transition-transform duration-200 ease-in-out',\n                column.getIsSorted() === 'desc' ? 'rotate-180' : 'rotate-0',\n              )}\n            />\n          ) : (\n            <ArrowUpDown className=\"size-3.5 opacity-40 transition-opacity duration-150 group-hover:opacity-70\" />\n          )}\n        </button>\n      ) : (\n        <span className=\"text-muted-foreground text-sm font-medium\">{label}</span>\n      )}\n\n      {/* Optional per-column header filter */}\n      {filter && (\n        <Popover open={open} onOpenChange={setOpen}>\n          <PopoverTrigger asChild>\n            <button\n              type=\"button\"\n              className={cn(\n                'text-muted-foreground relative inline-flex size-6 items-center justify-center rounded transition-colors',\n                'hover:text-foreground hover:bg-muted/60',\n                isFilterActive && 'text-foreground',\n              )}\n              aria-label={`Filter ${label}`}\n            >\n              <Filter className=\"size-3.5\" />\n              {isFilterActive && (\n                <span aria-hidden className=\"bg-primary absolute -top-0.5 -right-0.5 size-1.5 rounded-full\" />\n              )}\n            </button>\n          </PopoverTrigger>\n          <PopoverContent className=\"w-72 p-0\" align=\"start\">\n            <div className=\"border-border flex items-center justify-between border-b px-3 py-2 text-xs font-medium\">\n              <span>Filter · {filter.label}</span>\n              {isFilterActive && (\n                <button\n                  type=\"button\"\n                  className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1 transition-colors\"\n                  onClick={clearFilter}\n                >\n                  <FilterX className=\"size-3\" />\n                  Clear\n                </button>\n              )}\n            </div>\n\n            {/* TEXT */}\n            {filter.type === 'text' && (\n              <div className=\"space-y-2 p-3\">\n                <Input\n                  value={textDraft}\n                  placeholder={`Search ${filter.label.toLowerCase()}…`}\n                  className=\"h-9\"\n                  onChange={(e) => setTextDraft(e.target.value)}\n                  onKeyDown={(e) => {\n                    if (e.key === 'Enter') {\n                      applyText()\n                      setOpen(false)\n                    }\n                  }}\n                  onBlur={applyText}\n                />\n                <div className=\"flex gap-2\">\n                  <Button\n                    size=\"sm\"\n                    className=\"h-8 flex-1\"\n                    onClick={() => {\n                      applyText()\n                      setOpen(false)\n                    }}\n                  >\n                    Apply\n                  </Button>\n                  <Button size=\"sm\" variant=\"outline\" className=\"h-8\" onClick={clearText}>\n                    Clear\n                  </Button>\n                </div>\n              </div>\n            )}\n\n            {/* SELECT / MULTISELECT */}\n            {(filter.type === 'select' || filter.type === 'multiselect') && (\n              <Command className=\"max-h-[280px]\">\n                <CommandInput placeholder={`Search ${filter.label.toLowerCase()}…`} className=\"h-9\" />\n                <CommandList>\n                  <CommandEmpty>No matches.</CommandEmpty>\n                  <CommandGroup>\n                    {(filter.options ?? []).map((opt) => {\n                      const o = resolveOption(opt)\n                      return (\n                        <CommandItem\n                          key={o.value}\n                          value={o.value}\n                          onSelect={() =>\n                            filter.type === 'multiselect' ? toggleMultiselect(o.value) : selectOne(o.value)\n                          }\n                        >\n                          {filter.type === 'multiselect' ? (\n                            <div\n                              className={cn(\n                                'border-primary/50 mr-2 flex size-4 items-center justify-center rounded-sm border transition-colors',\n                                ((filterValue as string[]) ?? []).includes(o.value)\n                                  ? 'bg-primary text-primary-foreground'\n                                  : 'opacity-50',\n                              )}\n                            >\n                              <Check className=\"size-3\" />\n                            </div>\n                          ) : (\n                            <Check className={cn('mr-2 size-4', filterValue === o.value ? 'opacity-100' : 'opacity-0')} />\n                          )}\n                          <span>{o.label}</span>\n                        </CommandItem>\n                      )\n                    })}\n                  </CommandGroup>\n                </CommandList>\n              </Command>\n            )}\n\n            {/* DATE RANGE */}\n            {filter.type === 'date' && (\n              <div className=\"p-2\">\n                <RangeCalendar selected={dateModel} onSelect={onDateSelect} />\n                <div className=\"flex gap-2 px-1 pt-2\">\n                  <Button size=\"sm\" className=\"h-8 flex-1\" onClick={() => setOpen(false)}>\n                    Apply\n                  </Button>\n                  <Button size=\"sm\" variant=\"outline\" className=\"h-8\" onClick={clearFilter}>\n                    Clear\n                  </Button>\n                </div>\n              </div>\n            )}\n\n            {/* Active selection summary */}\n            {isFilterActive && (filter.type === 'multiselect' || filter.type === 'select') && (\n              <div className=\"border-border text-muted-foreground border-t px-3 py-2 text-[11px]\">\n                {filter.type === 'multiselect' ? (\n                  <span>\n                    {selectedLabels().length} selected:{' '}\n                    <span className=\"text-foreground\">{selectedLabels().join(', ')}</span>\n                  </span>\n                ) : (\n                  <span>\n                    <span className=\"text-foreground\">{filterValue as string}</span>\n                  </span>\n                )}\n              </div>\n            )}\n          </PopoverContent>\n        </Popover>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/DataTableColumnHeader.tsx"
    },
    {
      "path": "packages/registry-react/components/data-table/DataTableFilterPopover.tsx",
      "content": "'use client'\n\n/**\n * Popover variant of DataTableFilterSheet. Same filter UI (text /\n * multiselect / date) packed into a Popover instead of a side Sheet.\n * Slot in via `filterMode=\"popover\"` on <DataTable>.\n *\n * Trigger button is rendered inline by the toolbar; this component owns\n * the popover surface and content.\n *\n * ── Draft semantics ──────────────────────────────────────────────────\n * Unlike the Sheet (which mutates live TanStack column filters and rolls\n * back on cancel via a parent snapshot), this popover stages every edit\n * inside a local `draft` map. Real `columnFilters` are never touched\n * until the user clicks Apply -- at which point we emit `onCommitDraft`\n * with the full draft and the parent walks the entries calling\n * `setFilterValue` per column. Closing the popover (Escape / click out)\n * simply discards the draft.\n */\nimport * as React from 'react'\nimport type { Table } from '@tanstack/react-table'\nimport type { DateRange } from 'react-day-picker'\nimport { Check, SlidersHorizontal, X } from 'lucide-react'\nimport { Input } from '@/components/ui/input'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Label } from '@/components/ui/label'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { RangeCalendar } from '@/components/ui/range-calendar'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'\nimport { type FilterDefinition, resolveOption } from './types'\nimport { isoToDate, dateToIso } from './date-utils'\n\ntype DraftDateValue = { from?: string; to?: string }\ntype DraftValue = string[] | string | DraftDateValue | undefined\ntype Draft = Record<string, DraftValue>\n\nexport interface DataTableFilterPopoverProps {\n  table: Table<any>\n  filters: FilterDefinition[]\n  activeFilterCount: number\n  isAnyFilterActive: boolean\n  isServerSide: boolean\n  getMultiSelectValue: (column: string) => string[]\n  getDateRangeValue: (column: string) => { from?: string; to?: string }\n  formatDateRange: (column: string) => string\n  getCalendarModel: (column: string) => DateRange | undefined\n  // Committed draft on Apply -- a `Record<columnId, value>` where each\n  // value is the final shape TanStack's `setFilterValue` expects.\n  onCommitDraft: (draft: Draft) => void\n  onClearAll: () => void\n  customFilters?: React.ReactNode\n}\n\nexport function DataTableFilterPopover({\n  table,\n  filters,\n  activeFilterCount,\n  isAnyFilterActive: _isAnyFilterActive,\n  isServerSide,\n  getMultiSelectValue,\n  getDateRangeValue,\n  formatDateRange: _formatDateRange,\n  getCalendarModel: _getCalendarModel,\n  onCommitDraft,\n  onClearAll: _onClearAll,\n  customFilters,\n}: DataTableFilterPopoverProps) {\n  const [open, setOpen] = React.useState(false)\n  const [draft, setDraft] = React.useState<Draft>({})\n  const filterScrollRef = React.useRef<HTMLDivElement | null>(null)\n\n  function seedDraft(): Draft {\n    const next: Draft = {}\n    for (const f of filters) {\n      if (f.type === 'multiselect' || f.type === 'select') {\n        next[f.column] = [...getMultiSelectValue(f.column)]\n      } else if (f.type === 'date') {\n        const dr = getDateRangeValue(f.column)\n        next[f.column] = { from: dr.from, to: dr.to }\n      } else if (f.type === 'text') {\n        const v = table.getColumn(f.column)?.getFilterValue() as string | undefined\n        next[f.column] = v ?? ''\n      }\n    }\n    return next\n  }\n\n  function getDraftMulti(column: string): string[] {\n    const v = draft[column]\n    return Array.isArray(v) ? v : []\n  }\n\n  function getDraftText(column: string): string {\n    const v = draft[column]\n    return typeof v === 'string' ? v : ''\n  }\n\n  function getDraftDate(column: string): DraftDateValue {\n    const v = draft[column]\n    if (v && typeof v === 'object' && !Array.isArray(v)) return v as DraftDateValue\n    return {}\n  }\n\n  function toggleDraftMulti(column: string, option: string) {\n    const current = getDraftMulti(column)\n    const next = current.includes(option) ? current.filter((v) => v !== option) : [...current, option]\n    setDraft((d) => ({ ...d, [column]: next }))\n  }\n\n  function setDraftText(column: string, value: string) {\n    setDraft((d) => ({ ...d, [column]: value }))\n  }\n\n  function clearDraftSection(filter: FilterDefinition) {\n    if (filter.type === 'multiselect' || filter.type === 'select') {\n      setDraft((d) => ({ ...d, [filter.column]: [] }))\n    } else if (filter.type === 'date') {\n      setDraft((d) => ({ ...d, [filter.column]: {} }))\n    } else if (filter.type === 'text') {\n      setDraft((d) => ({ ...d, [filter.column]: '' }))\n    }\n  }\n\n  function resetDraft() {\n    const next: Draft = {}\n    for (const f of filters) {\n      if (f.type === 'multiselect' || f.type === 'select') next[f.column] = []\n      else if (f.type === 'date') next[f.column] = {}\n      else if (f.type === 'text') next[f.column] = ''\n    }\n    setDraft(next)\n  }\n\n  // ── Date helpers (local; popover is fully self-contained for draft) ──\n  function getDraftCalendarModel(column: string): DateRange | undefined {\n    const dr = getDraftDate(column)\n    if (!dr.from && !dr.to) return undefined\n    return {\n      from: isoToDate(dr.from),\n      to: isoToDate(dr.to),\n    }\n  }\n\n  function onDraftCalendarUpdate(column: string, val: DateRange | undefined) {\n    const from = val?.from ? dateToIso(val.from) : undefined\n    const to = val?.to ? dateToIso(val.to) : undefined\n    setDraft((d) => ({ ...d, [column]: { from, to } }))\n  }\n\n  function formatDraftDateRange(column: string): string {\n    const dr = getDraftDate(column)\n    if (dr.from && dr.to) return `${dr.from} - ${dr.to}`\n    if (dr.from) return `From ${dr.from}`\n    if (dr.to) return `Until ${dr.to}`\n    return ''\n  }\n\n  function isDraftSectionActive(filter: FilterDefinition): boolean {\n    if (filter.type === 'multiselect' || filter.type === 'select') {\n      return getDraftMulti(filter.column).length > 0\n    }\n    if (filter.type === 'date') {\n      const dr = getDraftDate(filter.column)\n      return !!(dr.from || dr.to)\n    }\n    if (filter.type === 'text') {\n      return !!getDraftText(filter.column)\n    }\n    return false\n  }\n\n  // `Reset` is disabled when the draft has nothing to clear.\n  const isDraftDirty = filters.some(isDraftSectionActive)\n\n  function handleOpenChange(isOpen: boolean) {\n    if (isOpen) {\n      setDraft(seedDraft())\n      setOpen(true)\n      setTimeout(() => filterScrollRef.current?.scrollTo({ top: 0 }), 50)\n    } else {\n      // Close-without-apply: nothing to commit. Real column filters were never\n      // mutated by the popover; the draft is local and discarded.\n      setDraft({})\n      setOpen(false)\n    }\n  }\n\n  function applyAndClose() {\n    const snapshot: Draft = {}\n    for (const f of filters) {\n      const v = draft[f.column]\n      if (f.type === 'multiselect' || f.type === 'select') {\n        const arr = Array.isArray(v) ? v : []\n        snapshot[f.column] = arr.length > 0 ? arr : undefined\n      } else if (f.type === 'date') {\n        const dr = v && typeof v === 'object' && !Array.isArray(v) ? (v as DraftDateValue) : {}\n        snapshot[f.column] = dr.from || dr.to ? { from: dr.from, to: dr.to } : undefined\n      } else if (f.type === 'text') {\n        const s = typeof v === 'string' ? v : ''\n        snapshot[f.column] = s || undefined\n      }\n    }\n    onCommitDraft(snapshot)\n    setOpen(false)\n  }\n\n  const filteredRowCount = table.getFilteredRowModel().rows.length\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange}>\n      <PopoverTrigger asChild>\n        <Button variant=\"outline\" size=\"sm\" className=\"h-9 gap-2\">\n          <SlidersHorizontal className=\"size-3.5\" />\n          Filters\n          {activeFilterCount > 0 && (\n            <Badge variant=\"secondary\" className=\"ml-1 h-5 min-w-5 rounded-full px-1.5 text-xs font-semibold\">\n              {activeFilterCount}\n            </Badge>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"flex max-h-[min(560px,80vh)] w-[380px] flex-col overflow-hidden p-0\">\n        {/* Header */}\n        <div className=\"border-b px-4 pt-3.5 pb-3\">\n          <div className=\"flex items-center gap-2.5\">\n            <div className=\"bg-muted flex size-7 items-center justify-center rounded-md\">\n              <SlidersHorizontal className=\"text-muted-foreground size-3.5\" />\n            </div>\n            <div className=\"flex-1\">\n              <p className=\"text-sm leading-none font-semibold\">Filters</p>\n              <p className=\"text-muted-foreground mt-1 text-xs\">\n                {activeFilterCount > 0 ? (\n                  <>\n                    {activeFilterCount} active\n                    {!isServerSide && (\n                      <>\n                        {' '}&middot; {filteredRowCount} result{filteredRowCount !== 1 ? 's' : ''}\n                      </>\n                    )}\n                  </>\n                ) : (\n                  'Narrow down results'\n                )}\n              </p>\n            </div>\n          </div>\n        </div>\n\n        {/* Scrollable filter sections */}\n        <div ref={filterScrollRef} className=\"flex-1 overflow-y-auto\">\n          <div className=\"space-y-2 p-3\">\n            {filters.map((filter) => {\n              if (filter.type === 'text') {\n                return (\n                  <div key={filter.column} className=\"bg-muted/40 rounded-lg p-2.5\">\n                    <div className=\"mb-2 flex items-center justify-between\">\n                      <Label className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">\n                        {filter.label}\n                      </Label>\n                      {getDraftText(filter.column) && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                          onClick={() => clearDraftSection(filter)}\n                        >\n                          Clear\n                        </button>\n                      )}\n                    </div>\n                    <Input\n                      placeholder={`Filter by ${filter.label.toLowerCase()}...`}\n                      value={getDraftText(filter.column)}\n                      className=\"h-8 text-sm\"\n                      onChange={(e) => setDraftText(filter.column, e.target.value ?? '')}\n                    />\n                  </div>\n                )\n              }\n\n              if (filter.type === 'multiselect' || filter.type === 'select') {\n                const multi = getDraftMulti(filter.column)\n                return (\n                  <div\n                    key={filter.column}\n                    className={[\n                      'rounded-lg p-2.5 transition-colors',\n                      multi.length > 0 ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n                    ].join(' ')}\n                  >\n                    <div className=\"mb-2 flex items-center justify-between\">\n                      <div className=\"flex items-center gap-2\">\n                        <Label className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">\n                          {filter.label}\n                        </Label>\n                        {multi.length > 0 && (\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"bg-primary/15 text-primary h-4 rounded-full px-1.5 text-xs font-semibold\"\n                          >\n                            {multi.length}\n                          </Badge>\n                        )}\n                      </div>\n                      {multi.length > 0 && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                          onClick={() => clearDraftSection(filter)}\n                        >\n                          Clear\n                        </button>\n                      )}\n                    </div>\n                    <Command className=\"[&_[data-slot=command-input-wrapper]]:border-input overflow-visible bg-transparent [&_[data-slot=command-input-wrapper]]:h-8 [&_[data-slot=command-input-wrapper]]:rounded-md [&_[data-slot=command-input-wrapper]]:border [&_[data-slot=command-input-wrapper]]:px-2.5\">\n                      <CommandInput className=\"h-7 text-sm\" placeholder={`Search ${filter.label.toLowerCase()}...`} />\n                      <CommandList className=\"mt-1 max-h-[132px]\">\n                        <CommandEmpty>No results.</CommandEmpty>\n                        <CommandGroup className=\"p-0\">\n                          {filter.options?.map((rawOpt) => {\n                            const opt = resolveOption(rawOpt)\n                            const OptIcon = opt.icon\n                            return (\n                              <CommandItem\n                                key={opt.value}\n                                value={opt.label}\n                                className=\"rounded-md px-2 py-1.5 text-sm\"\n                                onSelect={() => toggleDraftMulti(filter.column, opt.value)}\n                              >\n                                <div\n                                  className={[\n                                    'flex size-4 shrink-0 items-center justify-center rounded-sm border transition-colors',\n                                    multi.includes(opt.value)\n                                      ? 'border-primary bg-primary text-primary-foreground'\n                                      : 'border-muted-foreground/40 [&_svg]:invisible',\n                                  ].join(' ')}\n                                >\n                                  <Check className=\"size-3\" />\n                                </div>\n                                {OptIcon && <OptIcon className=\"text-muted-foreground size-4\" />}\n                                <span>{opt.label}</span>\n                              </CommandItem>\n                            )\n                          })}\n                        </CommandGroup>\n                      </CommandList>\n                    </Command>\n                  </div>\n                )\n              }\n\n              if (filter.type === 'date') {\n                const dr = getDraftDate(filter.column)\n                const hasDate = !!(dr.from || dr.to)\n                return (\n                  <div\n                    key={filter.column}\n                    className={[\n                      'rounded-lg p-2.5 transition-colors',\n                      hasDate ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n                    ].join(' ')}\n                  >\n                    <div className=\"mb-2 flex items-center justify-between\">\n                      <div className=\"flex items-center gap-2\">\n                        <Label className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">\n                          {filter.label}\n                        </Label>\n                        {hasDate && (\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"bg-primary/15 text-primary h-auto rounded-full px-1.5 py-0 text-xs font-medium\"\n                          >\n                            {formatDraftDateRange(filter.column)}\n                          </Badge>\n                        )}\n                      </div>\n                      {hasDate && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                          onClick={() => clearDraftSection(filter)}\n                        >\n                          Clear\n                        </button>\n                      )}\n                    </div>\n                    <div className=\"flex justify-center overflow-hidden rounded-md border\">\n                      <RangeCalendar\n                        selected={getDraftCalendarModel(filter.column)}\n                        numberOfMonths={1}\n                        className=\"p-2\"\n                        onSelect={(range) => onDraftCalendarUpdate(filter.column, range)}\n                      />\n                    </div>\n                  </div>\n                )\n              }\n\n              return null\n            })}\n\n            {/* Consumer-supplied custom filter UI */}\n            {customFilters}\n          </div>\n        </div>\n\n        {/* Footer */}\n        <div className=\"flex gap-2 border-t px-3 py-2.5\">\n          <Button variant=\"outline\" size=\"sm\" className=\"h-8 flex-1\" disabled={!isDraftDirty} onClick={resetDraft}>\n            <X className=\"size-3.5\" />\n            Reset\n          </Button>\n          <Button size=\"sm\" className=\"h-8 flex-1\" onClick={applyAndClose}>\n            Apply\n          </Button>\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/DataTableFilterPopover.tsx"
    },
    {
      "path": "packages/registry-react/components/data-table/DataTableFilterSheet.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport type { Table } from '@tanstack/react-table'\nimport type { DateRange } from 'react-day-picker'\nimport { Check, SlidersHorizontal, X } from 'lucide-react'\nimport { Input } from '@/components/ui/input'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Label } from '@/components/ui/label'\nimport { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet'\nimport { RangeCalendar } from '@/components/ui/range-calendar'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'\nimport { type FilterDefinition, resolveOption } from './types'\n\nexport interface DataTableFilterSheetProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  table: Table<any>\n  filters: FilterDefinition[]\n  activeFilterCount: number\n  isAnyFilterActive: boolean\n  isServerSide: boolean\n  /** Strip section bgs / rings / dividers / SheetContent side border. */\n  borderless?: boolean\n  getMultiSelectValue: (column: string) => string[]\n  getDateRangeValue: (column: string) => { from?: string; to?: string }\n  formatDateRange: (column: string) => string\n  getCalendarModel: (column: string) => DateRange | undefined\n  onApply: () => void\n  onClearAll: () => void\n  onToggleMultiselect: (column: string, value: string) => void\n  onClearFilter: (filter: FilterDefinition) => void\n  onClearDateFilter: (filter: FilterDefinition) => void\n  onCalendarUpdate: (column: string, value: DateRange | undefined) => void\n  onTextFilterUpdate: (column: string, value: string | undefined) => void\n  customFilters?: React.ReactNode\n}\n\nexport function DataTableFilterSheet({\n  open,\n  onOpenChange,\n  table,\n  filters,\n  activeFilterCount,\n  isAnyFilterActive,\n  isServerSide,\n  borderless = false,\n  getMultiSelectValue,\n  getDateRangeValue,\n  formatDateRange,\n  getCalendarModel,\n  onApply,\n  onClearAll,\n  onToggleMultiselect,\n  onClearFilter,\n  onClearDateFilter,\n  onCalendarUpdate,\n  onTextFilterUpdate,\n  customFilters,\n}: DataTableFilterSheetProps) {\n  const filterScrollRef = React.useRef<HTMLDivElement | null>(null)\n\n  React.useEffect(() => {\n    if (open) {\n      setTimeout(() => {\n        filterScrollRef.current?.scrollTo({ top: 0 })\n      }, 50)\n    }\n  }, [open])\n\n  const filteredRowCount = table.getFilteredRowModel().rows.length\n\n  return (\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className={['flex flex-col gap-0 p-0 sm:max-w-[400px]', borderless ? 'border-0' : ''].join(' ')}>\n        {/* Header */}\n        <div className={borderless ? 'px-5 pt-5 pb-2' : 'border-b px-5 pt-5 pb-4'}>\n          <div className=\"flex items-center gap-3\">\n            <div className=\"bg-muted flex size-9 items-center justify-center rounded-lg\">\n              <SlidersHorizontal className=\"text-muted-foreground size-4\" />\n            </div>\n            <div className=\"flex-1\">\n              <SheetHeader className=\"space-y-0.5 p-0\">\n                <SheetTitle className=\"text-sm font-semibold\">Filters</SheetTitle>\n                <SheetDescription className=\"text-xs\">\n                  {activeFilterCount > 0 ? (\n                    <>\n                      {activeFilterCount} active filter{activeFilterCount > 1 ? 's' : ''}\n                      {!isServerSide && (\n                        <>\n                          {' '}&middot; {filteredRowCount} result{filteredRowCount !== 1 ? 's' : ''}\n                        </>\n                      )}\n                    </>\n                  ) : (\n                    'Narrow down results'\n                  )}\n                </SheetDescription>\n              </SheetHeader>\n            </div>\n          </div>\n        </div>\n\n        {/* Scrollable filter sections */}\n        <div ref={filterScrollRef} className=\"flex-1 overflow-y-auto\">\n          <div className=\"space-y-2 p-4\">\n            {filters.map((filter) => {\n              const textValue = (table.getColumn(filter.column)?.getFilterValue() as string) ?? ''\n              const multi = getMultiSelectValue(filter.column)\n              const dr = getDateRangeValue(filter.column)\n              const hasDate = !!(dr.from || dr.to)\n\n              if (filter.type === 'text') {\n                return (\n                  <div key={filter.column} className={borderless ? 'py-2' : 'bg-muted/40 rounded-lg p-3'}>\n                    <div className=\"mb-2 flex items-center justify-between\">\n                      <Label className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">\n                        {filter.label}\n                      </Label>\n                      {textValue && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                          onClick={() => onTextFilterUpdate(filter.column, undefined)}\n                        >\n                          Clear\n                        </button>\n                      )}\n                    </div>\n                    <Input\n                      placeholder={`Filter by ${filter.label.toLowerCase()}...`}\n                      value={textValue}\n                      className=\"h-8 text-sm\"\n                      onChange={(e) => onTextFilterUpdate(filter.column, e.target.value || undefined)}\n                    />\n                  </div>\n                )\n              }\n\n              if (filter.type === 'multiselect' || filter.type === 'select') {\n                return (\n                  <div\n                    key={filter.column}\n                    className={\n                      borderless\n                        ? 'py-2 transition-colors'\n                        : [\n                            'rounded-lg p-3 transition-colors',\n                            multi.length > 0 ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n                          ].join(' ')\n                    }\n                  >\n                    <div className=\"mb-2 flex items-center justify-between\">\n                      <div className=\"flex items-center gap-2\">\n                        <Label className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">\n                          {filter.label}\n                        </Label>\n                        {multi.length > 0 && (\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"bg-primary/15 text-primary h-4 rounded-full px-1.5 text-xs font-semibold\"\n                          >\n                            {multi.length}\n                          </Badge>\n                        )}\n                      </div>\n                      {multi.length > 0 && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                          onClick={() => onClearFilter(filter)}\n                        >\n                          Clear\n                        </button>\n                      )}\n                    </div>\n                    <Command className=\"[&_[data-slot=command-input-wrapper]]:border-input overflow-visible bg-transparent [&_[data-slot=command-input-wrapper]]:h-8 [&_[data-slot=command-input-wrapper]]:rounded-md [&_[data-slot=command-input-wrapper]]:border [&_[data-slot=command-input-wrapper]]:px-2.5\">\n                      <CommandInput className=\"h-7 text-sm\" placeholder={`Search ${filter.label.toLowerCase()}...`} />\n                      <CommandList className=\"mt-1 max-h-[132px]\">\n                        <CommandEmpty>No results.</CommandEmpty>\n                        <CommandGroup className=\"p-0\">\n                          {filter.options?.map((rawOpt) => {\n                            const opt = resolveOption(rawOpt)\n                            const OptIcon = opt.icon\n                            return (\n                              <CommandItem\n                                key={opt.value}\n                                value={opt.label}\n                                className=\"rounded-md px-2 py-1.5 text-sm\"\n                                onSelect={() => onToggleMultiselect(filter.column, opt.value)}\n                              >\n                                <div\n                                  className={[\n                                    'flex size-4 shrink-0 items-center justify-center rounded-sm border transition-colors',\n                                    multi.includes(opt.value)\n                                      ? 'border-primary bg-primary text-primary-foreground'\n                                      : 'border-muted-foreground/40 [&_svg]:invisible',\n                                  ].join(' ')}\n                                >\n                                  <Check className=\"size-3\" />\n                                </div>\n                                {OptIcon && <OptIcon className=\"text-muted-foreground size-4\" />}\n                                <span>{opt.label}</span>\n                              </CommandItem>\n                            )\n                          })}\n                        </CommandGroup>\n                      </CommandList>\n                    </Command>\n                  </div>\n                )\n              }\n\n              if (filter.type === 'date') {\n                return (\n                  <div\n                    key={filter.column}\n                    className={\n                      borderless\n                        ? 'py-2 transition-colors'\n                        : [\n                            'rounded-lg p-3 transition-colors',\n                            hasDate ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n                          ].join(' ')\n                    }\n                  >\n                    <div className=\"mb-2 flex items-center justify-between\">\n                      <div className=\"flex items-center gap-2\">\n                        <Label className=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">\n                          {filter.label}\n                        </Label>\n                        {hasDate && (\n                          <Badge\n                            variant=\"secondary\"\n                            className=\"bg-primary/15 text-primary h-auto rounded-full px-1.5 py-0 text-xs font-medium\"\n                          >\n                            {formatDateRange(filter.column)}\n                          </Badge>\n                        )}\n                      </div>\n                      {hasDate && (\n                        <button\n                          type=\"button\"\n                          className=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                          onClick={() => onClearDateFilter(filter)}\n                        >\n                          Clear\n                        </button>\n                      )}\n                    </div>\n                    <div className={['flex justify-center overflow-hidden', borderless ? '' : 'rounded-md border'].join(' ')}>\n                      <RangeCalendar\n                        selected={getCalendarModel(filter.column)}\n                        numberOfMonths={1}\n                        className=\"p-2\"\n                        onSelect={(range) => onCalendarUpdate(filter.column, range)}\n                      />\n                    </div>\n                  </div>\n                )\n              }\n\n              return null\n            })}\n\n            {/* Consumer-supplied custom filter UI */}\n            {customFilters}\n          </div>\n        </div>\n\n        {/* Footer */}\n        <div className={['flex gap-2 px-4 py-3', borderless ? '' : 'border-t'].join(' ')}>\n          <Button variant=\"outline\" size=\"sm\" className=\"flex-1\" disabled={!isAnyFilterActive} onClick={onClearAll}>\n            <X className=\"size-3.5\" />\n            Reset All\n          </Button>\n          <Button size=\"sm\" className=\"flex-1\" onClick={onApply}>\n            {isServerSide ? (\n              'Apply Filters'\n            ) : (\n              <>\n                Show {filteredRowCount} result{filteredRowCount !== 1 ? 's' : ''}\n              </>\n            )}\n          </Button>\n        </div>\n      </SheetContent>\n    </Sheet>\n  )\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/DataTableFilterSheet.tsx"
    },
    {
      "path": "packages/registry-react/components/data-table/DataTablePagination.tsx",
      "content": "'use client'\n\nimport type { Table } from '@tanstack/react-table'\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\n\nexport interface DataTablePaginationProps {\n  table: Table<any>\n  totalRows: number\n  isServerSide: boolean\n  borderless?: boolean\n}\n\nexport function DataTablePagination({ table, totalRows, isServerSide, borderless = false }: DataTablePaginationProps) {\n  return (\n    <div className={['flex items-center justify-between py-3', borderless ? '' : 'border-t px-4'].join(' ')}>\n      <div className=\"text-muted-foreground flex-1 text-sm\">\n        {table.getFilteredSelectedRowModel().rows.length} of{' '}\n        {isServerSide ? totalRows : table.getFilteredRowModel().rows.length} row(s) selected.\n      </div>\n      <div className=\"flex items-center gap-6 lg:gap-8\">\n        <div className=\"flex items-center gap-2\">\n          <p className=\"text-sm font-medium whitespace-nowrap\">Rows per page</p>\n          <Select\n            value={`${table.getState().pagination.pageSize}`}\n            onValueChange={(value) => {\n              table.setPageSize(Number(value))\n              table.setPageIndex(0)\n            }}\n          >\n            <SelectTrigger className=\"h-8 w-[70px]\">\n              <SelectValue placeholder={`${table.getState().pagination.pageSize}`} />\n            </SelectTrigger>\n            <SelectContent side=\"top\">\n              {[10, 20, 30, 40, 50].map((pageSize) => (\n                <SelectItem key={pageSize} value={`${pageSize}`}>\n                  {pageSize}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"flex items-center justify-center text-sm font-medium whitespace-nowrap\">\n          Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}\n        </div>\n        <div className=\"flex items-center gap-1\">\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className=\"size-8\"\n            disabled={!table.getCanPreviousPage()}\n            onClick={() => table.setPageIndex(0)}\n          >\n            <ChevronsLeft className=\"size-4\" aria-hidden=\"true\" />\n            <span className=\"sr-only\">First page</span>\n          </Button>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className=\"size-8\"\n            disabled={!table.getCanPreviousPage()}\n            onClick={() => table.previousPage()}\n          >\n            <ChevronLeft className=\"size-4\" aria-hidden=\"true\" />\n            <span className=\"sr-only\">Previous page</span>\n          </Button>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className=\"size-8\"\n            disabled={!table.getCanNextPage()}\n            onClick={() => table.nextPage()}\n          >\n            <ChevronRight className=\"size-4\" aria-hidden=\"true\" />\n            <span className=\"sr-only\">Next page</span>\n          </Button>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className=\"size-8\"\n            disabled={!table.getCanNextPage()}\n            onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n          >\n            <ChevronsRight className=\"size-4\" aria-hidden=\"true\" />\n            <span className=\"sr-only\">Last page</span>\n          </Button>\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/DataTablePagination.tsx"
    },
    {
      "path": "packages/registry-react/components/data-table/DataTableToolbar.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport type { Table } from '@tanstack/react-table'\nimport type { DateRange } from 'react-day-picker'\nimport {\n  CalendarIcon,\n  Check,\n  ChevronDown,\n  Download,\n  ListFilter,\n  Plus,\n  Rows3,\n  SlidersHorizontal,\n  X,\n} from 'lucide-react'\nimport { Input } from '@/components/ui/input'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from '@/components/ui/command'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { RangeCalendar } from '@/components/ui/range-calendar'\nimport { DataTableFilterPopover } from './DataTableFilterPopover'\nimport { type FilterDefinition, resolveOption } from './types'\n\nexport interface DataTableToolbarProps {\n  table: Table<any>\n  filterColumn: string\n  filterPlaceholder: string\n  filters: FilterDefinition[]\n  filterMode: 'inline' | 'modal' | 'popover'\n  enableSearch?: boolean\n  enableColumnVisibility?: boolean\n  enableExport?: boolean\n  enableDensityToggle?: boolean\n  density?: 'compact' | 'cozy' | 'comfortable'\n  borderless?: boolean\n  activeFilterCount: number\n  isAnyFilterActive: boolean\n  isServerSide: boolean\n  getMultiSelectValue: (column: string) => string[]\n  getDateRangeValue: (column: string) => { from?: string; to?: string }\n  getFilterSelectedLabels: (filter: FilterDefinition) => string[]\n  formatDateRange: (column: string) => string\n  getCalendarModel: (column: string) => DateRange | undefined\n  onSearch: (value: string) => void\n  onOpenFilterSheet: () => void\n  onApplyFilters: () => void\n  onClearAllFilters: () => void\n  onToggleMultiselect: (column: string, value: string) => void\n  onClearFilter: (filter: FilterDefinition) => void\n  onClearDateFilter: (filter: FilterDefinition) => void\n  onCalendarUpdate: (column: string, value: DateRange | undefined) => void\n  onTextFilterUpdate: (column: string, value: string | undefined) => void\n  onCommitFilters: (draft: Record<string, any>) => void\n  onExportCsv: () => void\n  onExportJson: () => void\n  onDensityChange: (value: 'compact' | 'cozy' | 'comfortable') => void\n  toolbarExtra?: React.ReactNode\n  customFilters?: React.ReactNode\n}\n\nexport function DataTableToolbar({\n  table,\n  filterColumn,\n  filterPlaceholder,\n  filters,\n  filterMode,\n  enableSearch = true,\n  enableColumnVisibility = true,\n  enableExport = false,\n  enableDensityToggle = false,\n  density = 'cozy',\n  borderless = false,\n  activeFilterCount,\n  isAnyFilterActive,\n  isServerSide,\n  getMultiSelectValue,\n  getDateRangeValue,\n  getFilterSelectedLabels,\n  formatDateRange,\n  getCalendarModel,\n  onSearch,\n  onOpenFilterSheet,\n  onApplyFilters: _onApplyFilters,\n  onClearAllFilters,\n  onToggleMultiselect,\n  onClearFilter,\n  onClearDateFilter,\n  onCalendarUpdate,\n  onTextFilterUpdate,\n  onCommitFilters,\n  onExportCsv,\n  onExportJson,\n  onDensityChange,\n  toolbarExtra,\n  customFilters,\n}: DataTableToolbarProps) {\n  return (\n    <div className={['flex flex-col gap-2 py-3', borderless ? '' : 'border-b px-4'].join(' ')}>\n      <div className=\"flex items-center gap-2\">\n        {/* Search only renders when filterColumn maps to an actual column. */}\n        {enableSearch && filterColumn && table.getColumn(filterColumn) && (\n          <Input\n            className=\"h-9 max-w-xs\"\n            placeholder={filterPlaceholder}\n            value={(table.getColumn(filterColumn)?.getFilterValue() as string) ?? ''}\n            onChange={(e) => onSearch(e.target.value)}\n          />\n        )}\n\n        {/* ── INLINE filter mode ── */}\n        {filterMode === 'inline' &&\n          filters.map((filter) => {\n            if (filter.type === 'multiselect' || filter.type === 'select') {\n              const multi = getMultiSelectValue(filter.column)\n              return (\n                <Popover key={filter.column}>\n                  <PopoverTrigger asChild>\n                    <Button variant=\"outline\" size=\"sm\" className=\"h-9 border-dashed\">\n                      <Plus className=\"size-4\" aria-hidden=\"true\" />\n                      {filter.label}\n                      {multi.length > 0 && (\n                        <>\n                          <Separator orientation=\"vertical\" className=\"mx-1 h-4\" />\n                          <div className=\"flex gap-1\">\n                            {multi.length > 2 ? (\n                              <Badge variant=\"secondary\" className=\"rounded-sm px-1 font-normal\">\n                                {multi.length} selected\n                              </Badge>\n                            ) : (\n                              getFilterSelectedLabels(filter).map((label) => (\n                                <Badge key={label} variant=\"secondary\" className=\"rounded-sm px-1 font-normal\">\n                                  {label}\n                                </Badge>\n                              ))\n                            )}\n                          </div>\n                        </>\n                      )}\n                    </Button>\n                  </PopoverTrigger>\n                  <PopoverContent className=\"w-52 p-0\" align=\"start\">\n                    <Command>\n                      <CommandInput placeholder={`Search ${filter.label.toLowerCase()}...`} />\n                      <CommandList>\n                        <CommandEmpty>No results.</CommandEmpty>\n                        <CommandGroup>\n                          {filter.options?.map((rawOpt) => {\n                            const opt = resolveOption(rawOpt)\n                            const OptIcon = opt.icon\n                            return (\n                              <CommandItem\n                                key={opt.value}\n                                value={opt.label}\n                                onSelect={() => onToggleMultiselect(filter.column, opt.value)}\n                              >\n                                <div\n                                  className={[\n                                    'border-primary flex size-4 shrink-0 items-center justify-center rounded-sm border',\n                                    multi.includes(opt.value)\n                                      ? 'bg-primary text-primary-foreground'\n                                      : 'opacity-50 [&_svg]:invisible',\n                                  ].join(' ')}\n                                >\n                                  <Check className=\"size-3\" />\n                                </div>\n                                {OptIcon && <OptIcon className=\"text-muted-foreground size-4\" />}\n                                <span>{opt.label}</span>\n                              </CommandItem>\n                            )\n                          })}\n                        </CommandGroup>\n                        {multi.length > 0 && (\n                          <>\n                            <CommandSeparator />\n                            <CommandGroup>\n                              <CommandItem\n                                value=\"__clear__\"\n                                className=\"justify-center text-center\"\n                                onSelect={() => onClearFilter(filter)}\n                              >\n                                Clear filter\n                              </CommandItem>\n                            </CommandGroup>\n                          </>\n                        )}\n                      </CommandList>\n                    </Command>\n                  </PopoverContent>\n                </Popover>\n              )\n            }\n\n            if (filter.type === 'date') {\n              const dr = getDateRangeValue(filter.column)\n              const hasDate = !!(dr.from || dr.to)\n              return (\n                <Popover key={filter.column}>\n                  <PopoverTrigger asChild>\n                    <Button variant=\"outline\" size=\"sm\" className=\"h-9 border-dashed\">\n                      <CalendarIcon className=\"size-4\" aria-hidden=\"true\" />\n                      {filter.label}\n                      {hasDate && (\n                        <>\n                          <Separator orientation=\"vertical\" className=\"mx-1 h-4\" />\n                          <Badge variant=\"secondary\" className=\"rounded-sm px-1 font-normal\">\n                            {formatDateRange(filter.column)}\n                          </Badge>\n                        </>\n                      )}\n                    </Button>\n                  </PopoverTrigger>\n                  <PopoverContent className=\"w-auto p-0\" align=\"start\">\n                    <RangeCalendar\n                      selected={getCalendarModel(filter.column)}\n                      numberOfMonths={2}\n                      onSelect={(range) => onCalendarUpdate(filter.column, range)}\n                    />\n                    {hasDate && (\n                      <div className=\"border-t p-2\">\n                        <Button\n                          variant=\"ghost\"\n                          size=\"sm\"\n                          className=\"h-7 w-full text-xs\"\n                          onClick={() => onClearDateFilter(filter)}\n                        >\n                          Clear dates\n                        </Button>\n                      </div>\n                    )}\n                  </PopoverContent>\n                </Popover>\n              )\n            }\n\n            if (filter.type === 'text') {\n              const textValue = table.getColumn(filter.column)?.getFilterValue() as string | undefined\n              return (\n                <Popover key={filter.column}>\n                  <PopoverTrigger asChild>\n                    <Button variant=\"outline\" size=\"sm\" className=\"h-9 border-dashed\">\n                      <Plus className=\"size-4\" aria-hidden=\"true\" />\n                      {filter.label}\n                      {textValue && (\n                        <>\n                          <Separator orientation=\"vertical\" className=\"mx-1 h-4\" />\n                          <Badge variant=\"secondary\" className=\"rounded-sm px-1 font-normal\">\n                            {textValue}\n                          </Badge>\n                        </>\n                      )}\n                    </Button>\n                  </PopoverTrigger>\n                  <PopoverContent className=\"w-60 p-3\" align=\"start\">\n                    <div className=\"space-y-2\">\n                      <p className=\"text-sm font-medium\">{filter.label}</p>\n                      <Input\n                        placeholder={`Filter by ${filter.label.toLowerCase()}...`}\n                        value={(table.getColumn(filter.column)?.getFilterValue() as string) ?? ''}\n                        className=\"h-8 text-sm\"\n                        onChange={(e) => onTextFilterUpdate(filter.column, e.target.value || undefined)}\n                      />\n                    </div>\n                  </PopoverContent>\n                </Popover>\n              )\n            }\n\n            return null\n          })}\n\n        {/* ── MODAL filter mode (Sheet from the right) ── */}\n        {filterMode === 'modal' && (\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            className={[\n              'h-9',\n              activeFilterCount > 0 ? 'border-primary/40 bg-primary/5 text-primary hover:bg-primary/10' : '',\n            ].join(' ')}\n            onClick={onOpenFilterSheet}\n          >\n            <SlidersHorizontal className=\"size-4\" aria-hidden=\"true\" />\n            Filters\n            {activeFilterCount > 0 && (\n              <Badge className=\"bg-primary text-primary-foreground ml-0.5 size-5 rounded-full p-0 text-xs font-semibold\">\n                {activeFilterCount}\n              </Badge>\n            )}\n          </Button>\n        )}\n\n        {/* ── POPOVER filter mode ── */}\n        {filterMode === 'popover' && filters.length > 0 && (\n          <DataTableFilterPopover\n            table={table}\n            filters={filters}\n            activeFilterCount={activeFilterCount}\n            isAnyFilterActive={isAnyFilterActive}\n            isServerSide={isServerSide}\n            getMultiSelectValue={getMultiSelectValue}\n            getDateRangeValue={getDateRangeValue}\n            formatDateRange={formatDateRange}\n            getCalendarModel={getCalendarModel}\n            onCommitDraft={(d) => onCommitFilters(d)}\n            onClearAll={onClearAllFilters}\n            customFilters={customFilters}\n          />\n        )}\n\n        {/* Inline custom filters (when filterMode === 'inline') */}\n        {filterMode === 'inline' && customFilters}\n\n        {/* Toolbar extras (e.g. group-by selector, density toggle) */}\n        {toolbarExtra}\n\n        {/* Reset button */}\n        {isAnyFilterActive && (\n          <Button variant=\"ghost\" size=\"sm\" className=\"h-9\" onClick={onClearAllFilters}>\n            Reset\n            <X className=\"size-4\" aria-hidden=\"true\" />\n          </Button>\n        )}\n\n        {/* Export (CSV / JSON) */}\n        {enableExport && (\n          <div className=\"ml-auto\">\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <Button variant=\"outline\" size=\"sm\" className=\"h-9\">\n                  <Download className=\"size-3.5\" />\n                  Export\n                  <ChevronDown className=\"size-3.5 opacity-60\" />\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent align=\"end\">\n                <DropdownMenuItem onSelect={onExportCsv}>Export as CSV</DropdownMenuItem>\n                <DropdownMenuItem onSelect={onExportJson}>Export as JSON</DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n\n        {/* Density toggle */}\n        {enableDensityToggle && (\n          <div className={enableExport ? '' : 'ml-auto'}>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <Button variant=\"outline\" size=\"sm\" className=\"h-9\" aria-label={`Row density: ${density}`}>\n                  <Rows3 className=\"size-4\" aria-hidden=\"true\" />\n                  <span className=\"capitalize\">{density}</span>\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent align=\"end\">\n                <DropdownMenuRadioGroup\n                  value={density}\n                  onValueChange={(v) => onDensityChange(v as 'compact' | 'cozy' | 'comfortable')}\n                >\n                  <DropdownMenuRadioItem value=\"compact\">Compact</DropdownMenuRadioItem>\n                  <DropdownMenuRadioItem value=\"cozy\">Cozy</DropdownMenuRadioItem>\n                  <DropdownMenuRadioItem value=\"comfortable\">Comfortable</DropdownMenuRadioItem>\n                </DropdownMenuRadioGroup>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n\n        {/* Column Visibility */}\n        {enableColumnVisibility && (\n          <div className={enableExport || enableDensityToggle ? '' : 'ml-auto'}>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <Button variant=\"outline\" size=\"sm\" className=\"h-9\">\n                  <ListFilter className=\"size-4\" aria-hidden=\"true\" />\n                  View\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent align=\"end\">\n                {table\n                  .getAllColumns()\n                  .filter((column) => column.getCanHide())\n                  .map((column) => (\n                    <DropdownMenuCheckboxItem\n                      key={column.id}\n                      className=\"capitalize\"\n                      checked={column.getIsVisible()}\n                      onCheckedChange={(value) => column.toggleVisibility(!!value)}\n                    >\n                      {column.id}\n                    </DropdownMenuCheckboxItem>\n                  ))}\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/DataTableToolbar.tsx"
    },
    {
      "path": "packages/registry-react/components/data-table/types.ts",
      "content": "import type * as React from 'react'\n\nexport interface FilterOption {\n  value: string\n  label: string\n  icon?: React.ComponentType<{ className?: string }>\n}\n\nexport interface FilterDefinition {\n  column: string\n  label: string\n  type: 'text' | 'select' | 'multiselect' | 'date'\n  options?: (string | FilterOption)[]\n}\n\nexport function resolveOption(opt: string | FilterOption): FilterOption {\n  if (typeof opt === 'string') return { value: opt, label: opt }\n  return opt\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/types.ts"
    },
    {
      "path": "packages/registry-react/components/data-table/date-utils.ts",
      "content": "/**\n * ISO-string ↔ native Date helpers shared across the DataTable filter\n * surfaces. The Vue registry leaned on `@internationalized/date`'s\n * CalendarDate because its RangeCalendar (reka-ui) speaks DateValue; the\n * React RangeCalendar wraps react-day-picker, which speaks native `Date`.\n * So instead of pulling `@internationalized/date` into the React tree we\n * keep these two tiny converters local. Column filter values are still\n * stored as `YYYY-MM-DD` ISO strings on the wire, identical to Vue, so\n * server-side payloads match 1:1.\n */\nexport function isoToDate(str: string | undefined): Date | undefined {\n  if (!str) return undefined\n  const [y, m, d] = str.split('-').map(Number)\n  if (y === undefined || m === undefined || d === undefined) return undefined\n  return new Date(y, m - 1, d)\n}\n\nexport function dateToIso(date: Date | undefined): string {\n  if (!date) return ''\n  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/date-utils.ts"
    },
    {
      "path": "packages/registry-react/components/data-table/index.ts",
      "content": "export { DataTable, type DataTableProps, type DataTableHandle, type DataTableState } from './DataTable'\nexport { DataTableColumnHeader, type DataTableColumnHeaderProps } from './DataTableColumnHeader'\nexport { DataTableToolbar, type DataTableToolbarProps } from './DataTableToolbar'\nexport { DataTableFilterSheet, type DataTableFilterSheetProps } from './DataTableFilterSheet'\nexport { DataTableFilterPopover, type DataTableFilterPopoverProps } from './DataTableFilterPopover'\nexport { DataTablePagination, type DataTablePaginationProps } from './DataTablePagination'\nexport type { FilterDefinition, FilterOption } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/data-table/index.ts"
    }
  ],
  "dependencies": [
    "@tanstack/react-table",
    "react-day-picker",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/card.json",
    "https://uipkge.dev/r/react/command.json",
    "https://uipkge.dev/r/react/dropdown-menu.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/label.json",
    "https://uipkge.dev/r/react/popover.json",
    "https://uipkge.dev/r/react/range-calendar.json",
    "https://uipkge.dev/r/react/select.json",
    "https://uipkge.dev/r/react/separator.json",
    "https://uipkge.dev/r/react/sheet.json",
    "https://uipkge.dev/r/react/table.json"
  ],
  "description": "Full-feature table with sorting, filtering, column pinning, pagination, row selection, and an opinionated header/toolbar. Built on TanStack Table — pass `columns` + `data` and configure as needed.",
  "categories": [
    "data"
  ]
}