{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/data-table/DataTable.vue",
      "content": "<script setup lang=\"ts\" generic=\"TData, TValue\">\nimport type { ColumnDef, ColumnFiltersState, FilterFn, SortingState, VisibilityState, ExpandedState, GroupingState } from '@tanstack/vue-table'\nimport {\n  FlexRender,\n  getCoreRowModel,\n  getExpandedRowModel,\n  getFilteredRowModel,\n  getGroupedRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useVueTable,\n} from '@tanstack/vue-table'\n\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 { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'\nimport { CalendarDate, type DateValue } from '@internationalized/date'\nimport type { DateRange as RekaDateRange } from 'reka-ui'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'\n\nimport DataTableToolbar from './DataTableToolbar.vue'\nimport DataTableFilterSheet from './DataTableFilterSheet.vue'\nimport DataTablePagination from './DataTablePagination.vue'\n\nexport interface FilterOption {\n  value: string\n  label: string\n  icon?: any\n}\n\nexport interface FilterDefinition {\n  column: string\n  label: string\n  type: 'text' | 'select' | 'multiselect' | 'date'\n  options?: (string | FilterOption)[]\n}\n\ninterface DateRange {\n  from?: string\n  to?: string\n}\n\nconst props = withDefaults(\n  defineProps<{\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 true. */\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: emits `fetch-more` 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 `max-height` for a scroll container. */\n    virtual?: boolean\n    /** Sticky header — keeps `<thead>` visible when scrolling. Pair with `max-height`. */\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 (search / filters / export / view) renders.\n     *  - `'inside'` (default) is the canonical layout: toolbar lives inside\n     *    the bordered container above the table, separated by a divider.\n     *  - `'above'` floats the toolbar outside the bordered container, so\n     *    the table's border only wraps the table (and pagination unless\n     *    that is also moved out). Useful when you want the chrome to\n     *    feel like a page action bar rather than card-internal. */\n    toolbarPosition?: 'inside' | 'above'\n    /** Where the pagination footer renders.\n     *  - `'inside'` (default): footer sits inside the bordered container,\n     *    separated by a top-divider.\n     *  - `'below'` floats the footer outside the bordered container. */\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  }>(),\n  {\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    toolbarPosition: 'inside',\n    paginationPosition: 'inside',\n    maxHeight: '',\n    totalRows: -1,\n    loading: false,\n  },\n)\n\n// User-mutable density -- initial value from prop, toggleable via toolbar\n// when `enableDensityToggle` is on. Watch the prop so parents can still\n// drive it externally (e.g. saving the user's last choice to a setting).\nconst currentDensity = ref<'compact' | 'cozy' | 'comfortable'>(props.density)\nwatch(() => props.density, (v) => { currentDensity.value = v })\n\n// `borderless` is an enum at the DataTable level (`'inner'` vs `'full'`), but\n// child components only need to know \"should I drop my borders?\" -- collapse\n// to a single boolean for the pass-through.\nconst dropInnerBorders = computed(() => !!props.borderless)\n\nconst densityClass = computed(() => {\n  if (currentDensity.value === 'compact') return '[&_td]:py-1.5 [&_td]:px-3 [&_td]:text-xs [&_th]:h-8 [&_th]:px-3 [&_th]:text-xs'\n  if (currentDensity.value === 'comfortable') return '[&_td]:py-3 [&_th]:h-12'\n  // cozy -- 8px vertical cell padding, 40px header. Tightens the\n  // TableCell/TableHead defaults (py-3 / h-12) so a cozy DataTable\n  // doesn't feel airy compared with the surrounding canvas.\n  return '[&_td]:py-2 [&_th]:h-10'\n})\n\nconst emit = defineEmits<{\n  /** Emitted when server-side state changes (pagination, sorting, filters) */\n  (\n    e: 'update:state',\n    state: {\n      page: number\n      pageSize: number\n      sortBy: string\n      sortOrder: 'asc' | 'desc'\n      filters: Record<string, any>\n      search: string\n    },\n  ): void\n  /** Infinite-scroll: last row entered viewport, time to load more */\n  (e: 'fetch-more'): void\n}>()\n\nconst isServerSide = computed(() => props.totalRows >= 0)\nconst isFilterSheetOpen = ref(false)\n\n// ── Filter snapshot (restore on close without apply) ─────────────────────────\n// Shared between the modal Sheet and the inline Popover. Both surfaces stage\n// edits in the live `columnFilters` ref (so the in-popover \"N selected\"\n// badges update as the user clicks). Closing without hitting Apply rolls\n// back via the snapshot.\nlet filterSnapshot: ColumnFiltersState | null = null\nlet dateRangeSnapshot: Record<string, RekaDateRange> | null = null\nlet filterApplied = false\n\nfunction snapshotFilters() {\n  filterApplied = false\n  filterSnapshot = JSON.parse(JSON.stringify(columnFilters.value))\n  dateRangeSnapshot = JSON.parse(JSON.stringify(dateRangeModels.value))\n}\n\nfunction maybeRestoreFilters() {\n  if (!filterApplied && filterSnapshot) {\n    columnFilters.value = filterSnapshot\n    dateRangeModels.value = dateRangeSnapshot ?? {}\n  }\n  filterSnapshot = null\n  dateRangeSnapshot = null\n}\n\nfunction onFilterSheetOpen() {\n  snapshotFilters()\n  isFilterSheetOpen.value = true\n}\n\nfunction onFilterSheetClose(open: boolean) {\n  if (!open) maybeRestoreFilters()\n  isFilterSheetOpen.value = open\n}\n\n// ── Timers for debounce / batching ──────────────────────────────────────────\nlet searchDebounceTimer: ReturnType<typeof setTimeout> | null = null\nlet paginationEmitTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction onSearchInput(val: string) {\n  if (!props.filterColumn || !table.getColumn(props.filterColumn)) return\n  table.getColumn(props.filterColumn)?.setFilterValue(val || undefined)\n  if (isServerSide.value) {\n    if (searchDebounceTimer) clearTimeout(searchDebounceTimer)\n    searchDebounceTimer = setTimeout(() => {\n      table.setPageIndex(0)\n      emitStateUpdate()\n    }, 500)\n  }\n}\n\n// Custom filter functions for multiselect and date range\nconst 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\nconst dateRangeFilterFn: FilterFn<TData> = (row, columnId, filterValue: DateRange) => {\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\nconst processedColumns = computed(() => {\n  return props.columns.map((col) => {\n    const colId = (col as any).accessorKey || (col as any).id\n    const filter = props.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})\n\nconst sorting = ref<SortingState>([])\nconst columnFilters = ref<ColumnFiltersState>([])\nconst columnVisibility = ref<VisibilityState>({})\nconst rowSelection = ref({})\nconst expanded = ref<ExpandedState>({})\nconst columnPinning = ref({\n  left: props.defaultColumnPinning.left ?? [],\n  right: props.defaultColumnPinning.right ?? [],\n})\nconst columnOrder = ref<string[]>([])\nconst grouping = ref<GroupingState>(props.defaultGrouping)\nconst pagination = ref({ pageIndex: 0, pageSize: 10 })\n\nconst table = useVueTable({\n  get data() {\n    return props.data\n  },\n  get columns() {\n    return processedColumns.value\n  },\n  getCoreRowModel: getCoreRowModel(),\n  getPaginationRowModel: isServerSide.value ? undefined : getPaginationRowModel(),\n  getSortedRowModel: isServerSide.value ? undefined : getSortedRowModel(),\n  getFilteredRowModel: isServerSide.value ? undefined : getFilteredRowModel(),\n  getExpandedRowModel: getExpandedRowModel(),\n  getGroupedRowModel: getGroupedRowModel(),\n  get enableColumnResizing() {\n    return props.enableResize\n  },\n  columnResizeMode: 'onChange',\n  get manualPagination() {\n    return isServerSide.value\n  },\n  get manualSorting() {\n    return isServerSide.value\n  },\n  get manualFiltering() {\n    return isServerSide.value\n  },\n  get rowCount() {\n    return isServerSide.value ? props.totalRows : undefined\n  },\n  onSortingChange: (updaterOrValue) => {\n    valueUpdater(updaterOrValue, sorting)\n    if (isServerSide.value) emitStateUpdate()\n  },\n  onColumnFiltersChange: (updaterOrValue) => {\n    valueUpdater(updaterOrValue, columnFilters)\n    // Server-side: never auto-emit on filter change — handled by Apply button / search debounce\n    // Client-side inline: filters apply locally via TanStack, no emit needed\n  },\n  onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),\n  onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),\n  onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),\n  onPaginationChange: (updaterOrValue) => {\n    valueUpdater(updaterOrValue, pagination)\n    if (isServerSide.value) {\n      // Debounce to batch rapid pagination changes (e.g. setPageSize + setPageIndex)\n      if (paginationEmitTimer) clearTimeout(paginationEmitTimer)\n      paginationEmitTimer = setTimeout(() => emitStateUpdate(), 0)\n    }\n  },\n  state: {\n    get sorting() {\n      return sorting.value\n    },\n    get columnFilters() {\n      return columnFilters.value\n    },\n    get columnVisibility() {\n      return columnVisibility.value\n    },\n    get pagination() {\n      return pagination.value\n    },\n    get rowSelection() {\n      return rowSelection.value\n    },\n    get expanded() {\n      return expanded.value\n    },\n    get columnPinning() {\n      return columnPinning.value\n    },\n    get columnOrder() {\n      return columnOrder.value\n    },\n    get grouping() {\n      return grouping.value\n    },\n  },\n  onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinning),\n  onColumnOrderChange: updaterOrValue => valueUpdater(updaterOrValue, columnOrder),\n  onGroupingChange: updaterOrValue => valueUpdater(updaterOrValue, grouping),\n})\n\n/** Build and emit current server-side state */\nfunction emitStateUpdate() {\n  const s = sorting.value[0]\n  const filterMap: Record<string, any> = {}\n  for (const cf of columnFilters.value) {\n    filterMap[cf.id] = cf.value\n  }\n  emit('update:state', {\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: (props.filterColumn && (table.getColumn(props.filterColumn)?.getFilterValue() as string)) || '',\n  })\n}\n\n// ── Filter helpers ───────────────────────────────────────────────────────────\n\nfunction getMultiSelectValue(column: string): string[] {\n  return (table.getColumn(column)?.getFilterValue() as string[]) ?? []\n}\n\nfunction getDateRangeValue(column: string): DateRange {\n  return (table.getColumn(column)?.getFilterValue() as DateRange) ?? {}\n}\n\n// ── Date conversion helpers ──────────────────────────────────────────────────\n\nfunction stringToDateValue(str: string): DateValue | 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 CalendarDate(y, m, d)\n}\n\nfunction dateValueToString(dv: DateValue | undefined): string {\n  if (!dv) return ''\n  return `${dv.year}-${String(dv.month).padStart(2, '0')}-${String(dv.day).padStart(2, '0')}`\n}\n\n/** Per-filter reactive range calendar model, keyed by column name */\nconst dateRangeModels = ref<Record<string, RekaDateRange>>({})\n\nfunction getCalendarModel(column: string): RekaDateRange {\n  if (!dateRangeModels.value[column]) {\n    const dr = getDateRangeValue(column)\n    dateRangeModels.value[column] = {\n      start: stringToDateValue(dr.from ?? '') as DateValue,\n      end: stringToDateValue(dr.to ?? '') as DateValue,\n    }\n  }\n  return dateRangeModels.value[column]\n}\n\nfunction onCalendarUpdate(column: string, val: RekaDateRange) {\n  dateRangeModels.value[column] = val\n  const from = val.start ? dateValueToString(val.start) : undefined\n  const to = val.end ? dateValueToString(val.end) : undefined\n  const hasValue = from || to\n  table.getColumn(column)?.setFilterValue(hasValue ? { from, to } : undefined)\n}\n\nfunction 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\nfunction 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\nfunction clearAllFilters() {\n  filterApplied = true\n  if (searchDebounceTimer) clearTimeout(searchDebounceTimer)\n  props.filters.forEach((f) => {\n    table.getColumn(f.column)?.setFilterValue(undefined)\n    if (f.type === 'date') {\n      dateRangeModels.value[f.column] = { start: undefined as any, end: undefined as any }\n    }\n  })\n  if (props.filterColumn && table.getColumn(props.filterColumn)) {\n    table.getColumn(props.filterColumn)?.setFilterValue(undefined)\n  }\n  if (isServerSide.value) {\n    table.setPageIndex(0)\n    nextTick(() => emitStateUpdate())\n  }\n}\n\nfunction 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 DateRange\n    return !!(d.from || d.to)\n  }\n  return true\n}\n\nconst isAnyFilterActive = computed(() => {\n  const hasSearchValue = !!(props.filterColumn && (table.getColumn(props.filterColumn)?.getFilterValue() as string))\n  return hasSearchValue || props.filters.some(isFilterActive)\n})\n\nfunction 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\nfunction resolveOption(opt: string | FilterOption): FilterOption {\n  if (typeof opt === 'string') return { value: opt, label: opt }\n  return opt\n}\n\nfunction clearFilter(filter: FilterDefinition) {\n  table.getColumn(filter.column)?.setFilterValue(undefined)\n}\n\nconst activeFilterCount = computed(() => {\n  return props.filters.filter(isFilterActive).length\n})\n\nfunction clearDateFilter(filter: FilterDefinition) {\n  clearFilter(filter)\n  dateRangeModels.value[filter.column] = { start: undefined as any, end: undefined as any }\n}\n\nfunction onApplyFilters() {\n  filterApplied = true\n  if (isServerSide.value) {\n    if (table.getState().pagination.pageIndex === 0) {\n      emitStateUpdate()\n    }\n    else {\n      table.setPageIndex(0)\n    }\n  }\n  isFilterSheetOpen.value = false\n}\n\n/**\n * Popover filter mode: handle the staged-edit commit. The popover never\n * mutates `columnFilters` during interaction — it builds a draft locally\n * and emits it here on Apply. Walk the draft, write each column's value\n * via `setFilterValue`, and sync the calendar model for date filters so\n * a subsequent reopen reflects the just-applied range.\n */\nfunction onCommitDraft(draft: Record<string, any>) {\n  for (const f of props.filters) {\n    if (!(f.column in draft)) continue\n    const val = draft[f.column]\n    if (f.type === 'date') {\n      const dr = (val ?? {}) as DateRange\n      dateRangeModels.value[f.column] = {\n        start: stringToDateValue(dr.from ?? '') as DateValue,\n        end: stringToDateValue(dr.to ?? '') as DateValue,\n      }\n      table.getColumn(f.column)?.setFilterValue(val)\n    }\n    else {\n      table.getColumn(f.column)?.setFilterValue(val)\n    }\n  }\n  // Reuse the same apply-side bookkeeping (server-side re-fetch +\n  // pagination reset). filterApplied flips so any stale Sheet snapshot\n  // doesn't try to roll back.\n  onApplyFilters()\n}\n\n// Column reorder — HTML5 drag/drop swaps the dragged column with the drop target.\nconst dragColId = ref<string | null>(null)\nconst dragOverColId = ref<string | null>(null)\n\nfunction onColDragStart(id: string, e: DragEvent) {\n  dragColId.value = 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\nfunction onColDragOver(targetId: string, e: DragEvent) {\n  if (!props.enableReorder || !dragColId.value) return\n  e.preventDefault()\n  dragOverColId.value = targetId\n  if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n}\n\nfunction onColDragEnd() {\n  document.body.style.cursor = ''\n  dragColId.value = null\n  dragOverColId.value = null\n}\n\nfunction onColDrop(targetId: string) {\n  const src = dragColId.value\n  onColDragEnd()\n  if (!src || src === targetId) return\n  const order = (columnOrder.value.length ? columnOrder.value : 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  columnOrder.value = order\n}\n\n// Pinning — return style for sticky pinned cells (header or body)\nfunction pinStyle(col: any): Record<string, string> | 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\nconst sentinelEl = ref<HTMLElement | null>(null)\nlet io: IntersectionObserver | null = null\n\nonMounted(() => {\n  if (!props.infinite || !sentinelEl.value) return\n  io = new IntersectionObserver(\n    (entries) => {\n      if (entries[0]?.isIntersecting) emit('fetch-more')\n    },\n    { rootMargin: '100px' },\n  )\n  io.observe(sentinelEl.value)\n})\n\nonBeforeUnmount(() => {\n  io?.disconnect()\n  io = null\n})\n\nwatch(\n  () => props.infinite,\n  (on) => {\n    if (on && sentinelEl.value && !io) {\n      io = new IntersectionObserver(\n        (entries) => {\n          if (entries[0]?.isIntersecting) emit('fetch-more')\n        },\n        { rootMargin: '100px' },\n      )\n      io.observe(sentinelEl.value)\n    }\n    else if (!on) {\n      io?.disconnect()\n      io = null\n    }\n  },\n)\n\n// CSV export — uses currently filtered + visible data, skips select/actions cols.\nfunction 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  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })\n  const url = URL.createObjectURL(blob)\n  const a = document.createElement('a')\n  a.href = url\n  a.download = `export-${Date.now()}.csv`\n  document.body.appendChild(a)\n  a.click()\n  document.body.removeChild(a)\n  URL.revokeObjectURL(url)\n}\n\n// JSON export -- same row/column scope as CSV, but emits the underlying\n// row.original objects so downstream tools get typed data, not strings.\nfunction 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    // When the page projects via accessorKey, original keys match. When it\n    // uses accessorFn (eg. a synthesized \"name\" column), surface that under\n    // the column id so consumers see the same shape they exported.\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  const blob = new Blob([json], { type: 'application/json;charset=utf-8;' })\n  const url = URL.createObjectURL(blob)\n  const a = document.createElement('a')\n  a.href = url\n  a.download = `export-${Date.now()}.json`\n  document.body.appendChild(a)\n  a.click()\n  document.body.removeChild(a)\n  URL.revokeObjectURL(url)\n}\n\ndefineExpose({ table, exportCsv, exportJson })\n</script>\n\n<template>\n  <!-- ClientOnly: the TanStack table renders identically on server and client\n       in principle, but its interactive Reka-based controls resolve internal\n       state in the browser, producing benign hydration mismatches. SSR of an\n       interactive data grid adds little, so render it client-side. -->\n  <ClientOnly>\n    <div\n      data-uipkge\n      data-slot=\"data-table\"\n      class=\"w-full\"\n    >\n      <!-- Toolbar (above-mode: floats outside the bordered card, no\n           bottom-divider since nothing sits directly below it inside the\n           same surface). -->\n      <DataTableToolbar\n        v-if=\"!hideToolbar && toolbarPosition === 'above'\"\n        :table=\"table\"\n        :filter-column=\"filterColumn\"\n        :filter-placeholder=\"filterPlaceholder\"\n        :filters=\"filters\"\n        :filter-mode=\"filterMode\"\n        :enable-search=\"enableSearch\"\n        :enable-column-visibility=\"enableColumnVisibility\"\n        :enable-export=\"enableExport\"\n        :enable-density-toggle=\"enableDensityToggle\"\n        :density=\"currentDensity\"\n        :borderless=\"true\"\n        :active-filter-count=\"activeFilterCount\"\n        :is-any-filter-active=\"isAnyFilterActive\"\n        :is-server-side=\"isServerSide\"\n        :get-multi-select-value=\"getMultiSelectValue\"\n        :get-date-range-value=\"getDateRangeValue\"\n        :get-filter-selected-labels=\"getFilterSelectedLabels\"\n        :format-date-range=\"formatDateRange\"\n        :get-calendar-model=\"getCalendarModel\"\n        @search=\"onSearchInput\"\n        @open-filter-sheet=\"onFilterSheetOpen\"\n        @apply-filters=\"onApplyFilters\"\n        @clear-all-filters=\"clearAllFilters\"\n        @toggle-multiselect=\"toggleMultiSelectValue\"\n        @clear-filter=\"clearFilter\"\n        @clear-date-filter=\"clearDateFilter\"\n        @calendar-update=\"onCalendarUpdate\"\n        @text-filter-update=\"(col, val) => table.getColumn(col)?.setFilterValue(val)\"\n        @commit-filters=\"onCommitDraft\"\n        @export-csv=\"exportCsv\"\n        @export-json=\"exportJson\"\n        @update:density=\"(d: 'compact' | 'cozy' | 'comfortable') => (currentDensity = d)\"\n      >\n        <template\n          v-if=\"$slots['toolbar-extra']\"\n          #toolbar-extra\n        >\n          <slot\n            name=\"toolbar-extra\"\n            :table=\"table\"\n          />\n        </template>\n        <template\n          v-if=\"$slots['custom-filters']\"\n          #custom-filters\n        >\n          <slot\n            name=\"custom-filters\"\n            :table=\"table\"\n          />\n        </template>\n      </DataTableToolbar>\n\n      <div\n        :class=\"[\n          'bg-card text-card-foreground overflow-hidden',\n          borderless === 'full' ? '' : 'rounded-md border',\n        ]\"\n      >\n        <!-- Toolbar (inside-mode, default): canonical card layout, toolbar\n             sits at the top of the bordered card with its bottom-divider. -->\n        <DataTableToolbar\n          v-if=\"!hideToolbar && toolbarPosition === 'inside'\"\n          :table=\"table\"\n          :filter-column=\"filterColumn\"\n          :filter-placeholder=\"filterPlaceholder\"\n          :filters=\"filters\"\n          :filter-mode=\"filterMode\"\n          :enable-search=\"enableSearch\"\n          :enable-column-visibility=\"enableColumnVisibility\"\n          :enable-export=\"enableExport\"\n          :enable-density-toggle=\"enableDensityToggle\"\n          :density=\"currentDensity\"\n          :borderless=\"dropInnerBorders\"\n          :active-filter-count=\"activeFilterCount\"\n          :is-any-filter-active=\"isAnyFilterActive\"\n          :is-server-side=\"isServerSide\"\n          :get-multi-select-value=\"getMultiSelectValue\"\n          :get-date-range-value=\"getDateRangeValue\"\n          :get-filter-selected-labels=\"getFilterSelectedLabels\"\n          :format-date-range=\"formatDateRange\"\n          :get-calendar-model=\"getCalendarModel\"\n          @search=\"onSearchInput\"\n          @open-filter-sheet=\"onFilterSheetOpen\"\n          @apply-filters=\"onApplyFilters\"\n          @clear-all-filters=\"clearAllFilters\"\n          @toggle-multiselect=\"toggleMultiSelectValue\"\n          @clear-filter=\"clearFilter\"\n          @clear-date-filter=\"clearDateFilter\"\n          @calendar-update=\"onCalendarUpdate\"\n          @text-filter-update=\"(col, val) => table.getColumn(col)?.setFilterValue(val)\"\n          @commit-filters=\"onCommitDraft\"\n          @export-csv=\"exportCsv\"\n          @export-json=\"exportJson\"\n          @update:density=\"(d: 'compact' | 'cozy' | 'comfortable') => (currentDensity = d)\"\n        >\n          <template\n            v-if=\"$slots['toolbar-extra']\"\n            #toolbar-extra\n          >\n            <slot\n              name=\"toolbar-extra\"\n              :table=\"table\"\n            />\n          </template>\n          <template\n            v-if=\"$slots['custom-filters']\"\n            #custom-filters\n          >\n            <slot\n              name=\"custom-filters\"\n              :table=\"table\"\n            />\n          </template>\n        </DataTableToolbar>\n\n        <!-- Filter Sheet (modal mode) -->\n        <DataTableFilterSheet\n          :open=\"isFilterSheetOpen\"\n          :table=\"table\"\n          :filters=\"filters\"\n          :active-filter-count=\"activeFilterCount\"\n          :is-any-filter-active=\"isAnyFilterActive\"\n          :is-server-side=\"isServerSide\"\n          :borderless=\"dropInnerBorders\"\n          :get-multi-select-value=\"getMultiSelectValue\"\n          :get-date-range-value=\"getDateRangeValue\"\n          :format-date-range=\"formatDateRange\"\n          :get-calendar-model=\"getCalendarModel\"\n          @update:open=\"onFilterSheetClose\"\n          @apply=\"onApplyFilters\"\n          @clear-all=\"clearAllFilters\"\n          @toggle-multiselect=\"toggleMultiSelectValue\"\n          @clear-filter=\"clearFilter\"\n          @clear-date-filter=\"clearDateFilter\"\n          @calendar-update=\"onCalendarUpdate\"\n          @text-filter-update=\"(col, val) => table.getColumn(col)?.setFilterValue(val)\"\n        >\n          <template\n            v-if=\"$slots['custom-filters']\"\n            #custom-filters\n          >\n            <slot\n              name=\"custom-filters\"\n              :table=\"table\"\n            />\n          </template>\n        </DataTableFilterSheet>\n\n        <!-- Bulk action bar (slot above table when rows selected) -->\n        <div\n          v-if=\"$slots['bulk-actions'] && table.getSelectedRowModel().rows.length > 0\"\n          class=\"bg-muted/40 flex items-center gap-3 border-b px-4 py-2\"\n        >\n          <span class=\"text-sm font-medium\"> {{ table.getSelectedRowModel().rows.length }} selected </span>\n          <slot\n            name=\"bulk-actions\"\n            :rows=\"table.getSelectedRowModel().rows\"\n            :clear=\"() => table.toggleAllRowsSelected(false)\"\n          />\n          <button\n            type=\"button\"\n            class=\"text-muted-foreground hover:text-foreground ml-auto text-xs transition\"\n            @click=\"table.toggleAllRowsSelected(false)\"\n          >\n            Clear selection\n          </button>\n        </div>\n\n        <!-- Table.\n           The Table primitive wraps the <table> in its own overflow-auto div\n           which would normally trap sticky-positioned children + draw native\n           scrollbars. Neutralize that inner overflow and let\n           OverlayScrollbarsComponent own both axes so we get a single,\n           themed, auto-hiding pair of scrollbars across the whole region. -->\n        <div\n          v-os-scroll\n          :class=\"[densityClass, 'relative [&_[data-slot=table-container]]:overflow-visible']\"\n          :style=\"maxHeight ? `max-height: ${maxHeight}` : ''\"\n        >\n          <!-- Loading overlay -->\n          <div\n            v-if=\"loading\"\n            class=\"bg-card/60 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-[1px]\"\n          >\n            <div class=\"border-primary size-5 animate-spin rounded-full border-2 border-t-transparent\" />\n          </div>\n          <Table>\n            <TableHeader :class=\"stickyHeader ? 'bg-muted/95 sticky top-0 z-10 backdrop-blur-sm' : ''\">\n              <TableRow\n                v-for=\"headerGroup in table.getHeaderGroups()\"\n                :key=\"headerGroup.id\"\n              >\n                <TableHead\n                  v-for=\"header in headerGroup.headers\"\n                  :key=\"header.id\"\n                  class=\"relative transition-colors duration-150\"\n                  :class=\"[\n                    enableReorder\n                      && header.column.id !== 'select'\n                      && header.column.id !== 'actions'\n                      && header.column.id !== 'expander'\n                      ? 'cursor-grab active:cursor-grabbing'\n                      : '',\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                  ]\"\n                  :style=\"{\n                    ...(enableResize ? { width: `${header.getSize()}px` } : {}),\n                    ...(pinStyle(header.column) ?? {}),\n                  }\"\n                  :draggable=\"\n                    enableReorder\n                      && header.column.id !== 'select'\n                      && header.column.id !== 'actions'\n                      && header.column.id !== 'expander'\n                  \"\n                  @dragstart=\"enableReorder ? onColDragStart(header.column.id, $event) : undefined\"\n                  @dragover=\"enableReorder ? onColDragOver(header.column.id, $event) : undefined\"\n                  @dragend=\"enableReorder ? onColDragEnd() : undefined\"\n                  @drop=\"enableReorder ? onColDrop(header.column.id) : undefined\"\n                >\n                  <FlexRender\n                    v-if=\"!header.isPlaceholder\"\n                    :render=\"header.column.columnDef.header\"\n                    :props=\"header.getContext()\"\n                  />\n                  <div\n                    v-if=\"enableResize && header.column.getCanResize()\"\n                    class=\"hover:bg-foreground/30 absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none transition-colors select-none\"\n                    :class=\"header.column.getIsResizing() ? 'bg-foreground/60' : ''\"\n                    @mousedown=\"header.getResizeHandler()?.($event)\"\n                    @touchstart=\"header.getResizeHandler()?.($event)\"\n                  />\n                </TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              <template v-if=\"table.getRowModel().rows?.length\">\n                <template\n                  v-for=\"row in table.getRowModel().rows\"\n                  :key=\"row.id\"\n                >\n                  <TableRow\n                    :data-state=\"row.getIsSelected() ? 'selected' : undefined\"\n                    :class=\"onRowClick ? 'hover:bg-muted/40 cursor-pointer' : ''\"\n                    :style=\"virtual ? 'content-visibility: auto; contain-intrinsic-size: auto 48px;' : undefined\"\n                    @click=\"onRowClick?.(row.original)\"\n                  >\n                    <TableCell\n                      v-for=\"cell in row.getVisibleCells()\"\n                      :key=\"cell.id\"\n                      class=\"bg-card\"\n                      :style=\"pinStyle(cell.column)\"\n                    >\n                      <FlexRender\n                        :render=\"cell.column.columnDef.cell\"\n                        :props=\"cell.getContext()\"\n                      />\n                    </TableCell>\n                  </TableRow>\n                  <TableRow\n                    v-if=\"row.getIsExpanded() && $slots.expanded\"\n                    :key=\"`${row.id}-expanded`\"\n                  >\n                    <TableCell\n                      :colspan=\"row.getVisibleCells().length\"\n                      class=\"bg-muted/30 px-6 py-4\"\n                    >\n                      <slot\n                        name=\"expanded\"\n                        :row=\"row.original\"\n                        :tanstack-row=\"row\"\n                      />\n                    </TableCell>\n                  </TableRow>\n                </template>\n              </template>\n              <template v-else>\n                <TableRow>\n                  <TableCell\n                    :colspan=\"columns.length\"\n                    class=\"h-24 text-center\"\n                  >\n                    <slot name=\"empty\">\n                      <div class=\"text-muted-foreground text-sm\">\n                        No results.\n                      </div>\n                    </slot>\n                  </TableCell>\n                </TableRow>\n              </template>\n            </TableBody>\n            <tfoot\n              v-if=\"$slots.footer\"\n              class=\"bg-muted/20 sticky bottom-0 border-t\"\n            >\n              <slot\n                name=\"footer\"\n                :rows=\"table.getRowModel().rows\"\n              />\n            </tfoot>\n          </Table>\n          <!-- Infinite scroll sentinel -->\n          <div\n            v-if=\"infinite\"\n            ref=\"sentinelEl\"\n            class=\"h-1\"\n          />\n          <div\n            v-if=\"infinite && loading\"\n            class=\"border-border text-muted-foreground border-t px-4 py-3 text-center text-sm\"\n          >\n            Loading more…\n          </div>\n        </div>\n\n        <!-- Pagination (inside-mode, default) -->\n        <DataTablePagination\n          v-if=\"enablePagination && !infinite && paginationPosition === 'inside'\"\n          :table=\"table\"\n          :total-rows=\"totalRows\"\n          :is-server-side=\"isServerSide\"\n          :borderless=\"dropInnerBorders\"\n        />\n      </div>\n\n      <!-- Pagination (below-mode: floats outside the bordered card,\n           dividerless since nothing sits directly above it on the same surface). -->\n      <DataTablePagination\n        v-if=\"enablePagination && !infinite && paginationPosition === 'below'\"\n        :table=\"table\"\n        :total-rows=\"totalRows\"\n        :is-server-side=\"isServerSide\"\n        :borderless=\"true\"\n      />\n    </div>\n  </ClientOnly>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/DataTable.vue"
    },
    {
      "path": "packages/registry-vue/components/data-table/DataTableColumnHeader.vue",
      "content": "<script setup lang=\"ts\" generic=\"TData, TValue\">\n/**\n * Sortable / hideable header cell for TanStack DataTable columns. Use it from\n * a column definition like:\n *   header: ({ column }) => h(DataTableColumnHeader, { column, label: 'Email' })\n *\n * Click on the label cycles sort asc → desc → none.\n *\n * Optional per-column filter:\n *   header: ({ column }) => h(DataTableColumnHeader, {\n *     column,\n *     label: 'Status',\n *     filter: { column: 'status', label: 'Status', type: 'multiselect', options: [...] }\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 { computed, ref, watch } from 'vue'\nimport type { Column } from '@tanstack/vue-table'\nimport { ArrowDown, ArrowUp, ArrowUpDown, Check, Filter, FilterX } from 'lucide-vue-next'\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 { parseDate, CalendarDate, type DateValue } from '@internationalized/date'\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\nconst props = defineProps<{\n  column: Column<TData, TValue>\n  label: string\n  align?: 'left' | 'right' | 'center'\n  filter?: FilterDefinition\n  class?: string\n}>()\n\nconst align = computed(() => props.align ?? 'left')\n\nfunction next() {\n  const current = props.column.getIsSorted()\n  if (!current) props.column.toggleSorting(false)\n  else if (current === 'asc') props.column.toggleSorting(true)\n  else props.column.clearSorting()\n}\n\nfunction resolveOption(opt: string | FilterOption): FilterOption {\n  return typeof opt === 'string' ? { value: opt, label: opt } : opt\n}\n\n// Filter state read directly off the TanStack column so the popover stays\n// reactive to external clears (toolbar \"Clear all\", row-level edits, etc.).\nconst open = ref(false)\nconst filterValue = computed(() => props.column.getFilterValue())\n\nconst isFilterActive = computed(() => {\n  const v = filterValue.value\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.\nconst textDraft = ref('')\nwatch(open, (isOpen) => {\n  if (isOpen && props.filter?.type === 'text') {\n    textDraft.value = (filterValue.value as string) ?? ''\n  }\n})\n\nfunction applyText() {\n  props.column.setFilterValue(textDraft.value || undefined)\n}\n\nfunction clearText() {\n  textDraft.value = ''\n  props.column.setFilterValue(undefined)\n}\n\nfunction toggleMultiselect(value: string) {\n  const current = (filterValue.value as string[]) ?? []\n  const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]\n  props.column.setFilterValue(next.length > 0 ? next : undefined)\n}\n\nfunction selectOne(value: string) {\n  props.column.setFilterValue(value || undefined)\n  open.value = false\n}\n\nfunction clearFilter() {\n  props.column.setFilterValue(undefined)\n  textDraft.value = ''\n}\n\n// Date-range bridging. The column stores ISO strings; the calendar wants\n// CalendarDate instances. Reads/writes both shapes.\nconst dateModel = computed({\n  get() {\n    const v = filterValue.value as { from?: string; to?: string } | undefined\n    if (!v?.from && !v?.to) return undefined\n    try {\n      return {\n        start: v.from ? parseDate(v.from) : undefined,\n        end: v.to ? parseDate(v.to) : undefined,\n      }\n    } catch {\n      return undefined\n    }\n  },\n  set(next: { start?: DateValue; end?: DateValue } | undefined) {\n    const isoFrom = next?.start ? (next.start as CalendarDate).toString() : undefined\n    const isoTo = next?.end ? (next.end as CalendarDate).toString() : undefined\n    if (!isoFrom && !isoTo) {\n      props.column.setFilterValue(undefined)\n    } else {\n      props.column.setFilterValue({ from: isoFrom, to: isoTo })\n    }\n  },\n})\n\nfunction selectedLabels(): string[] {\n  const f = props.filter\n  if (!f?.options) return []\n  const selected = (filterValue.value as string[]) ?? []\n  return f.options\n    .map(resolveOption)\n    .filter((o) => selected.includes(o.value))\n    .map((o) => o.label)\n}\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'flex items-center gap-0.5',\n        align === 'right' && 'justify-end',\n        align === 'center' && 'justify-center',\n        props.class,\n      )\n    \"\n  >\n    <button\n      type=\"button\"\n      v-if=\"column.getCanSort()\"\n      class=\"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      @click=\"next\"\n    >\n      <span>{{ label }}</span>\n      <ArrowUp\n        v-if=\"column.getIsSorted()\"\n        class=\"text-foreground size-3.5 transition-transform duration-200 ease-in-out\"\n        :class=\"column.getIsSorted() === 'desc' ? 'rotate-180' : 'rotate-0'\"\n      />\n      <ArrowUpDown v-else class=\"size-3.5 opacity-40 transition-opacity duration-150 group-hover:opacity-70\" />\n    </button>\n    <span v-else class=\"text-muted-foreground text-sm font-medium\">{{ label }}</span>\n\n    <!-- Optional per-column header filter -->\n    <Popover v-if=\"filter\" v-model:open=\"open\">\n      <PopoverTrigger as-child>\n        <button\n          type=\"button\"\n          :class=\"\n            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          \"\n          :aria-label=\"`Filter ${label}`\"\n        >\n          <Filter class=\"size-3.5\" />\n          <span\n            v-if=\"isFilterActive\"\n            aria-hidden\n            class=\"bg-primary absolute -top-0.5 -right-0.5 size-1.5 rounded-full\"\n          />\n        </button>\n      </PopoverTrigger>\n      <PopoverContent class=\"w-72 p-0\" align=\"start\">\n        <div class=\"border-border flex items-center justify-between border-b px-3 py-2 text-xs font-medium\">\n          <span>Filter · {{ filter.label }}</span>\n          <button\n            type=\"button\"\n            v-if=\"isFilterActive\"\n            class=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1 transition-colors\"\n            @click=\"clearFilter\"\n          >\n            <FilterX class=\"size-3\" />\n            Clear\n          </button>\n        </div>\n\n        <!-- TEXT -->\n        <div v-if=\"filter.type === 'text'\" class=\"space-y-2 p-3\">\n          <Input\n            v-model=\"textDraft\"\n            :placeholder=\"`Search ${filter.label.toLowerCase()}…`\"\n            class=\"h-9\"\n            @keydown.enter=\"\n              () => {\n                applyText()\n                open = false\n              }\n            \"\n            @blur=\"applyText\"\n          />\n          <div class=\"flex gap-2\">\n            <Button\n              size=\"sm\"\n              class=\"h-8 flex-1\"\n              @click=\"\n                () => {\n                  applyText()\n                  open = false\n                }\n              \"\n              >Apply</Button\n            >\n            <Button size=\"sm\" variant=\"outline\" class=\"h-8\" @click=\"clearText()\">Clear</Button>\n          </div>\n        </div>\n\n        <!-- SELECT / MULTISELECT -->\n        <Command v-else-if=\"filter.type === 'select' || filter.type === 'multiselect'\" class=\"max-h-[280px]\">\n          <CommandInput :placeholder=\"`Search ${filter.label.toLowerCase()}…`\" class=\"h-9\" />\n          <CommandList>\n            <CommandEmpty>No matches.</CommandEmpty>\n            <CommandGroup>\n              <template v-for=\"opt in filter.options ?? []\" :key=\"resolveOption(opt).value\">\n                <CommandItem\n                  :value=\"resolveOption(opt).value\"\n                  @select=\"\n                    filter.type === 'multiselect'\n                      ? toggleMultiselect(resolveOption(opt).value)\n                      : selectOne(resolveOption(opt).value)\n                  \"\n                >\n                  <div\n                    v-if=\"filter.type === 'multiselect'\"\n                    :class=\"\n                      cn(\n                        'border-primary/50 mr-2 flex size-4 items-center justify-center rounded-sm border transition-colors',\n                        ((filterValue as string[]) ?? []).includes(resolveOption(opt).value)\n                          ? 'bg-primary text-primary-foreground'\n                          : 'opacity-50',\n                      )\n                    \"\n                  >\n                    <Check class=\"size-3\" />\n                  </div>\n                  <Check\n                    v-else\n                    :class=\"cn('mr-2 size-4', filterValue === resolveOption(opt).value ? 'opacity-100' : 'opacity-0')\"\n                  />\n                  <span>{{ resolveOption(opt).label }}</span>\n                </CommandItem>\n              </template>\n            </CommandGroup>\n          </CommandList>\n        </Command>\n\n        <!-- DATE RANGE -->\n        <div v-else-if=\"filter.type === 'date'\" class=\"p-2\">\n          <RangeCalendar v-model=\"dateModel as any\" />\n          <div class=\"flex gap-2 px-1 pt-2\">\n            <Button size=\"sm\" class=\"h-8 flex-1\" @click=\"open = false\">Apply</Button>\n            <Button size=\"sm\" variant=\"outline\" class=\"h-8\" @click=\"clearFilter\">Clear</Button>\n          </div>\n        </div>\n\n        <!-- Active selection summary -->\n        <div\n          v-if=\"isFilterActive && (filter.type === 'multiselect' || filter.type === 'select')\"\n          class=\"border-border text-muted-foreground border-t px-3 py-2 text-[11px]\"\n        >\n          <span v-if=\"filter.type === 'multiselect'\">\n            {{ selectedLabels().length }} selected:\n            <span class=\"text-foreground\">{{ selectedLabels().join(', ') }}</span>\n          </span>\n          <span v-else>\n            <span class=\"text-foreground\">{{ filterValue }}</span>\n          </span>\n        </div>\n      </PopoverContent>\n    </Popover>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/DataTableColumnHeader.vue"
    },
    {
      "path": "packages/registry-vue/components/data-table/DataTableFilterPopover.vue",
      "content": "<script setup lang=\"ts\">\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 `commit-draft`\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 *\n * Why: ticking a checkbox inside the popover used to write straight to\n * `columnFilters`, causing the underlying table to flash filtered before\n * the user committed. Truly staging the edits avoids that flash and\n * makes \"Reset\" / per-section \"Clear\" behave intuitively (they clear\n * the draft, not the live table).\n */\nimport type { Table } from '@tanstack/vue-table'\nimport type { FilterDefinition, FilterOption } from './DataTable.vue'\nimport type { DateValue } from '@internationalized/date'\nimport type { DateRange as RekaDateRange } from 'reka-ui'\nimport { computed, ref, watch } from 'vue'\nimport { CalendarDate } from '@internationalized/date'\n\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 { Check, SlidersHorizontal, X } from 'lucide-vue-next'\n\ntype DraftDateValue = { from?: string, to?: string }\ntype DraftValue = string[] | string | DraftDateValue | undefined\ntype Draft = Record<string, DraftValue>\n\nfunction resolveOption(opt: string | FilterOption): FilterOption {\n  if (typeof opt === 'string') return { value: opt, label: opt }\n  return opt\n}\n\nconst props = defineProps<{\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) => any\n}>()\n\nconst open = ref(false)\n\nconst emit = defineEmits<{\n  (e: 'clear-all'): void\n  // Committed draft on Apply -- a `Record<columnId, value>` where each\n  // value is the final shape TanStack's `setFilterValue` expects\n  // (multiselect: string[]|undefined, text: string|undefined,\n  //  date: {from?,to?}|undefined).\n  (e: 'commit-draft', draft: Draft): void\n  (e: 'open'): void\n}>()\n\n// ── Draft state ──────────────────────────────────────────────────────\n// Seeded from real column filter values when the popover opens; cleared\n// when it closes. All in-popover UI binds to this map -- never to the\n// live TanStack filter values directly.\nconst draft = ref<Draft>({})\n\nfunction seedDraft() {\n  const next: Draft = {}\n  for (const f of props.filters) {\n    if (f.type === 'multiselect' || f.type === 'select') {\n      next[f.column] = [...props.getMultiSelectValue(f.column)]\n    }\n    else if (f.type === 'date') {\n      const dr = props.getDateRangeValue(f.column)\n      next[f.column] = { from: dr.from, to: dr.to }\n    }\n    else if (f.type === 'text') {\n      const v = props.table.getColumn(f.column)?.getFilterValue() as string | undefined\n      next[f.column] = v ?? ''\n    }\n  }\n  draft.value = next\n}\n\nfunction getDraftMulti(column: string): string[] {\n  const v = draft.value[column]\n  return Array.isArray(v) ? v : []\n}\n\nfunction getDraftText(column: string): string {\n  const v = draft.value[column]\n  return typeof v === 'string' ? v : ''\n}\n\nfunction getDraftDate(column: string): DraftDateValue {\n  const v = draft.value[column]\n  if (v && typeof v === 'object' && !Array.isArray(v)) return v as DraftDateValue\n  return {}\n}\n\nfunction toggleDraftMulti(column: string, option: string) {\n  const current = getDraftMulti(column)\n  const next = current.includes(option) ? current.filter(v => v !== option) : [...current, option]\n  draft.value = { ...draft.value, [column]: next }\n}\n\nfunction setDraftText(column: string, value: string) {\n  draft.value = { ...draft.value, [column]: value }\n}\n\nfunction clearDraftSection(filter: FilterDefinition) {\n  if (filter.type === 'multiselect' || filter.type === 'select') {\n    draft.value = { ...draft.value, [filter.column]: [] }\n  }\n  else if (filter.type === 'date') {\n    draft.value = { ...draft.value, [filter.column]: {} }\n  }\n  else if (filter.type === 'text') {\n    draft.value = { ...draft.value, [filter.column]: '' }\n  }\n}\n\nfunction resetDraft() {\n  const next: Draft = {}\n  for (const f of props.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  draft.value = next\n}\n\n// ── Date helpers (local; popover is fully self-contained for draft) ──\nfunction stringToDateValue(str: string | undefined): DateValue | 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 CalendarDate(y, m, d)\n}\n\nfunction dateValueToString(dv: DateValue | undefined): string {\n  if (!dv) return ''\n  return `${dv.year}-${String(dv.month).padStart(2, '0')}-${String(dv.day).padStart(2, '0')}`\n}\n\nfunction getDraftCalendarModel(column: string): RekaDateRange {\n  const dr = getDraftDate(column)\n  return {\n    start: stringToDateValue(dr.from) as DateValue,\n    end: stringToDateValue(dr.to) as DateValue,\n  }\n}\n\nfunction onDraftCalendarUpdate(column: string, val: RekaDateRange) {\n  const from = val?.start ? dateValueToString(val.start) : undefined\n  const to = val?.end ? dateValueToString(val.end) : undefined\n  draft.value = { ...draft.value, [column]: { from, to } }\n}\n\nfunction 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// ── Active section detection (draft, not live) ───────────────────────\nfunction 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.\nconst isDraftDirty = computed(() => props.filters.some(isDraftSectionActive))\n\n// ── Popover lifecycle ────────────────────────────────────────────────\nwatch(open, (isOpen, wasOpen) => {\n  if (isOpen && !wasOpen) {\n    seedDraft()\n    emit('open')\n    return\n  }\n  if (!isOpen && wasOpen) {\n    // Close-without-apply: nothing to do. Real column filters were never\n    // mutated by the popover; the draft is local and can be left to\n    // garbage-collect (next open re-seeds from live state).\n    draft.value = {}\n  }\n})\n\nfunction applyAndClose() {\n  // Snapshot the draft so the parent receives a stable object even if\n  // the close watcher fires before they finish consuming it.\n  const snapshot: Draft = {}\n  for (const f of props.filters) {\n    const v = draft.value[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    }\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    }\n    else if (f.type === 'text') {\n      const s = typeof v === 'string' ? v : ''\n      snapshot[f.column] = s || undefined\n    }\n  }\n  emit('commit-draft', snapshot)\n  open.value = false\n}\n\nfunction onResetClick() {\n  resetDraft()\n}\n\nconst filterScrollRef = ref<HTMLElement | null>(null)\n\nwatch(open, (isOpen) => {\n  if (isOpen) {\n    setTimeout(() => {\n      filterScrollRef.value?.scrollTo({ top: 0 })\n    }, 50)\n  }\n})\n</script>\n\n<template>\n  <Popover v-model:open=\"open\">\n    <PopoverTrigger as-child>\n      <Button\n        variant=\"outline\"\n        size=\"sm\"\n        class=\"h-9 gap-2\"\n      >\n        <SlidersHorizontal class=\"size-3.5\" />\n        Filters\n        <Badge\n          v-if=\"activeFilterCount > 0\"\n          variant=\"secondary\"\n          class=\"ml-1 h-5 min-w-5 rounded-full px-1.5 text-xs font-semibold\"\n        >\n          {{ activeFilterCount }}\n        </Badge>\n      </Button>\n    </PopoverTrigger>\n    <PopoverContent\n      align=\"start\"\n      class=\"flex max-h-[min(560px,80vh)] w-[380px] flex-col overflow-hidden p-0\"\n    >\n      <!-- Header -->\n      <div class=\"border-b px-4 pt-3.5 pb-3\">\n        <div class=\"flex items-center gap-2.5\">\n          <div class=\"bg-muted flex size-7 items-center justify-center rounded-md\">\n            <SlidersHorizontal class=\"text-muted-foreground size-3.5\" />\n          </div>\n          <div class=\"flex-1\">\n            <p class=\"text-sm leading-none font-semibold\">\n              Filters\n            </p>\n            <p class=\"text-muted-foreground mt-1 text-xs\">\n              <template v-if=\"activeFilterCount > 0\">\n                {{ activeFilterCount }} active\n                <template v-if=\"!isServerSide\">\n                  &middot; {{ table.getFilteredRowModel().rows.length }} result{{\n                    table.getFilteredRowModel().rows.length !== 1 ? 's' : ''\n                  }}\n                </template>\n              </template>\n              <template v-else>\n                Narrow down results\n              </template>\n            </p>\n          </div>\n        </div>\n      </div>\n\n      <!-- Scrollable filter sections -->\n      <div\n        ref=\"filterScrollRef\"\n        class=\"flex-1 overflow-y-auto\"\n      >\n        <div class=\"space-y-2 p-3\">\n          <template\n            v-for=\"filter in filters\"\n            :key=\"filter.column\"\n          >\n            <!-- Text filter -->\n            <div\n              v-if=\"filter.type === 'text'\"\n              class=\"bg-muted/40 rounded-lg p-2.5\"\n            >\n              <div class=\"mb-2 flex items-center justify-between\">\n                <Label class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n                  filter.label\n                }}</Label>\n                <button\n                  v-if=\"getDraftText(filter.column)\"\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                  @click=\"clearDraftSection(filter)\"\n                >\n                  Clear\n                </button>\n              </div>\n              <Input\n                :placeholder=\"`Filter by ${filter.label.toLowerCase()}...`\"\n                :model-value=\"getDraftText(filter.column)\"\n                class=\"h-8 text-sm\"\n                @update:model-value=\"setDraftText(filter.column, ($event as string) ?? '')\"\n              />\n            </div>\n\n            <!-- Multiselect / Select filter -->\n            <div\n              v-else-if=\"filter.type === 'multiselect' || filter.type === 'select'\"\n              class=\"rounded-lg p-2.5 transition-colors\"\n              :class=\"[\n                getDraftMulti(filter.column).length > 0\n                  ? 'bg-primary/[0.04] ring-primary/20 ring-1'\n                  : 'bg-muted/40',\n              ]\"\n            >\n              <div class=\"mb-2 flex items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <Label class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n                    filter.label\n                  }}</Label>\n                  <Badge\n                    v-if=\"getDraftMulti(filter.column).length > 0\"\n                    variant=\"secondary\"\n                    class=\"bg-primary/15 text-primary h-4 rounded-full px-1.5 text-xs font-semibold\"\n                  >\n                    {{ getDraftMulti(filter.column).length }}\n                  </Badge>\n                </div>\n                <button\n                  v-if=\"getDraftMulti(filter.column).length > 0\"\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                  @click=\"clearDraftSection(filter)\"\n                >\n                  Clear\n                </button>\n              </div>\n              <Command\n                class=\"[&_[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              >\n                <CommandInput\n                  class=\"h-7 text-sm\"\n                  :placeholder=\"`Search ${filter.label.toLowerCase()}...`\"\n                />\n                <CommandList class=\"mt-1 max-h-[132px]\">\n                  <CommandEmpty>No results.</CommandEmpty>\n                  <CommandGroup class=\"p-0\">\n                    <CommandItem\n                      v-for=\"rawOpt in filter.options\"\n                      :key=\"resolveOption(rawOpt).value\"\n                      :value=\"resolveOption(rawOpt).label\"\n                      class=\"rounded-md px-2 py-1.5 text-sm\"\n                      @select=\"toggleDraftMulti(filter.column, resolveOption(rawOpt).value)\"\n                    >\n                      <div\n                        class=\"flex size-4 shrink-0 items-center justify-center rounded-sm border transition-colors\"\n                        :class=\"[\n                          getDraftMulti(filter.column).includes(resolveOption(rawOpt).value)\n                            ? 'border-primary bg-primary text-primary-foreground'\n                            : 'border-muted-foreground/40 [&_svg]:invisible',\n                        ]\"\n                      >\n                        <Check class=\"size-3\" />\n                      </div>\n                      <component\n                        :is=\"resolveOption(rawOpt).icon\"\n                        v-if=\"resolveOption(rawOpt).icon\"\n                        class=\"text-muted-foreground size-4\"\n                      />\n                      <span>{{ resolveOption(rawOpt).label }}</span>\n                    </CommandItem>\n                  </CommandGroup>\n                </CommandList>\n              </Command>\n            </div>\n\n            <!-- Date range filter -->\n            <div\n              v-else-if=\"filter.type === 'date'\"\n              class=\"rounded-lg p-2.5 transition-colors\"\n              :class=\"[\n                getDraftDate(filter.column).from || getDraftDate(filter.column).to\n                  ? 'bg-primary/[0.04] ring-primary/20 ring-1'\n                  : 'bg-muted/40',\n              ]\"\n            >\n              <div class=\"mb-2 flex items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <Label class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n                    filter.label\n                  }}</Label>\n                  <Badge\n                    v-if=\"getDraftDate(filter.column).from || getDraftDate(filter.column).to\"\n                    variant=\"secondary\"\n                    class=\"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                </div>\n                <button\n                  v-if=\"getDraftDate(filter.column).from || getDraftDate(filter.column).to\"\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                  @click=\"clearDraftSection(filter)\"\n                >\n                  Clear\n                </button>\n              </div>\n              <div class=\"flex justify-center overflow-hidden rounded-md border\">\n                <RangeCalendar\n                  :model-value=\"getDraftCalendarModel(filter.column)\"\n                  :number-of-months=\"1\"\n                  class=\"p-2\"\n                  @update:model-value=\"onDraftCalendarUpdate(filter.column, $event as RekaDateRange)\"\n                />\n              </div>\n            </div>\n          </template>\n\n          <!-- Consumer-supplied custom filter UI -->\n          <slot name=\"custom-filters\" />\n        </div>\n      </div>\n\n      <!-- Footer -->\n      <div class=\"flex gap-2 border-t px-3 py-2.5\">\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          class=\"h-8 flex-1\"\n          :disabled=\"!isDraftDirty\"\n          @click=\"onResetClick\"\n        >\n          <X class=\"size-3.5\" />\n          Reset\n        </Button>\n        <Button\n          size=\"sm\"\n          class=\"h-8 flex-1\"\n          @click=\"applyAndClose\"\n        >\n          Apply\n        </Button>\n      </div>\n    </PopoverContent>\n  </Popover>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/DataTableFilterPopover.vue"
    },
    {
      "path": "packages/registry-vue/components/data-table/DataTableFilterSheet.vue",
      "content": "<script setup lang=\"ts\">\nimport type { Table } from '@tanstack/vue-table'\nimport type { FilterDefinition, FilterOption } from './DataTable.vue'\nimport { ref, watch } from 'vue'\n\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 { Check, SlidersHorizontal, X } from 'lucide-vue-next'\n\nfunction resolveOption(opt: string | FilterOption): FilterOption {\n  if (typeof opt === 'string') return { value: opt, label: opt }\n  return opt\n}\n\nwithDefaults(\n  defineProps<{\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) => any\n  }>(),\n  { borderless: false },\n)\n\nconst open = defineModel<boolean>('open', { default: false })\n\nconst emit = defineEmits<{\n  (e: 'apply' | 'clear-all'): void\n  (e: 'toggle-multiselect', column: string, value: string): void\n  (e: 'clear-filter' | 'clear-date-filter', filter: FilterDefinition): void\n  (e: 'calendar-update', column: string, value: any): void\n  (e: 'text-filter-update', column: string, value: string | undefined): void\n}>()\n\nconst filterScrollRef = ref<HTMLElement | null>(null)\n\nwatch(open, (isOpen) => {\n  if (isOpen) {\n    setTimeout(() => {\n      filterScrollRef.value?.scrollTo({ top: 0 })\n    }, 50)\n  }\n})\n</script>\n\n<template>\n  <Sheet v-model:open=\"open\">\n    <SheetContent :class=\"['flex flex-col gap-0 p-0 sm:max-w-[400px]', borderless ? 'border-0' : '']\">\n      <!-- Header -->\n      <div :class=\"[borderless ? 'px-5 pt-5 pb-2' : 'border-b px-5 pt-5 pb-4']\">\n        <div class=\"flex items-center gap-3\">\n          <div class=\"bg-muted flex size-9 items-center justify-center rounded-lg\">\n            <SlidersHorizontal class=\"text-muted-foreground size-4\" />\n          </div>\n          <div class=\"flex-1\">\n            <SheetHeader class=\"space-y-0.5 p-0\">\n              <SheetTitle class=\"text-sm font-semibold\">\n                Filters\n              </SheetTitle>\n              <SheetDescription class=\"text-xs\">\n                <template v-if=\"activeFilterCount > 0\">\n                  {{ activeFilterCount }} active filter{{ activeFilterCount > 1 ? 's' : '' }}\n                  <template v-if=\"!isServerSide\">\n                    &middot;\n                    {{ table.getFilteredRowModel().rows.length }} result{{\n                      table.getFilteredRowModel().rows.length !== 1 ? 's' : ''\n                    }}\n                  </template>\n                </template>\n                <template v-else>\n                  Narrow down results\n                </template>\n              </SheetDescription>\n            </SheetHeader>\n          </div>\n        </div>\n      </div>\n\n      <!-- Scrollable filter sections -->\n      <div\n        ref=\"filterScrollRef\"\n        class=\"flex-1 overflow-y-auto\"\n      >\n        <div class=\"space-y-2 p-4\">\n          <template\n            v-for=\"filter in filters\"\n            :key=\"filter.column\"\n          >\n            <!-- Text filter -->\n            <div\n              v-if=\"filter.type === 'text'\"\n              :class=\"[borderless ? 'py-2' : 'bg-muted/40 rounded-lg p-3']\"\n            >\n              <div class=\"mb-2 flex items-center justify-between\">\n                <Label class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n                  filter.label\n                }}</Label>\n                <button\n                  v-if=\"table.getColumn(filter.column)?.getFilterValue() as string\"\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                  @click=\"emit('text-filter-update', filter.column, undefined)\"\n                >\n                  Clear\n                </button>\n              </div>\n              <Input\n                :placeholder=\"`Filter by ${filter.label.toLowerCase()}...`\"\n                :model-value=\"(table.getColumn(filter.column)?.getFilterValue() as string) ?? ''\"\n                class=\"h-8 text-sm\"\n                @update:model-value=\"emit('text-filter-update', filter.column, ($event as string) || undefined)\"\n              />\n            </div>\n\n            <!-- Multiselect / Select filter -->\n            <div\n              v-else-if=\"filter.type === 'multiselect' || filter.type === 'select'\"\n              :class=\"\n                borderless\n                  ? 'py-2 transition-colors'\n                  : [\n                    'rounded-lg p-3 transition-colors',\n                    getMultiSelectValue(filter.column).length > 0\n                      ? 'bg-primary/[0.04] ring-primary/20 ring-1'\n                      : 'bg-muted/40',\n                  ]\n              \"\n            >\n              <div class=\"mb-2 flex items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <Label class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n                    filter.label\n                  }}</Label>\n                  <Badge\n                    v-if=\"getMultiSelectValue(filter.column).length > 0\"\n                    variant=\"secondary\"\n                    class=\"bg-primary/15 text-primary h-4 rounded-full px-1.5 text-xs font-semibold\"\n                  >\n                    {{ getMultiSelectValue(filter.column).length }}\n                  </Badge>\n                </div>\n                <button\n                  v-if=\"getMultiSelectValue(filter.column).length > 0\"\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                  @click=\"emit('clear-filter', filter)\"\n                >\n                  Clear\n                </button>\n              </div>\n              <Command\n                class=\"[&_[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              >\n                <CommandInput\n                  class=\"h-7 text-sm\"\n                  :placeholder=\"`Search ${filter.label.toLowerCase()}...`\"\n                />\n                <CommandList class=\"mt-1 max-h-[132px]\">\n                  <CommandEmpty>No results.</CommandEmpty>\n                  <CommandGroup class=\"p-0\">\n                    <CommandItem\n                      v-for=\"rawOpt in filter.options\"\n                      :key=\"resolveOption(rawOpt).value\"\n                      :value=\"resolveOption(rawOpt).label\"\n                      class=\"rounded-md px-2 py-1.5 text-sm\"\n                      @select=\"emit('toggle-multiselect', filter.column, resolveOption(rawOpt).value)\"\n                    >\n                      <div\n                        class=\"flex size-4 shrink-0 items-center justify-center rounded-sm border transition-colors\"\n                        :class=\"[\n                          getMultiSelectValue(filter.column).includes(resolveOption(rawOpt).value)\n                            ? 'border-primary bg-primary text-primary-foreground'\n                            : 'border-muted-foreground/40 [&_svg]:invisible',\n                        ]\"\n                      >\n                        <Check class=\"size-3\" />\n                      </div>\n                      <component\n                        :is=\"resolveOption(rawOpt).icon\"\n                        v-if=\"resolveOption(rawOpt).icon\"\n                        class=\"text-muted-foreground size-4\"\n                      />\n                      <span>{{ resolveOption(rawOpt).label }}</span>\n                    </CommandItem>\n                  </CommandGroup>\n                </CommandList>\n              </Command>\n            </div>\n\n            <!-- Date range filter -->\n            <div\n              v-else-if=\"filter.type === 'date'\"\n              :class=\"\n                borderless\n                  ? 'py-2 transition-colors'\n                  : [\n                    'rounded-lg p-3 transition-colors',\n                    getDateRangeValue(filter.column).from || getDateRangeValue(filter.column).to\n                      ? 'bg-primary/[0.04] ring-primary/20 ring-1'\n                      : 'bg-muted/40',\n                  ]\n              \"\n            >\n              <div class=\"mb-2 flex items-center justify-between\">\n                <div class=\"flex items-center gap-2\">\n                  <Label class=\"text-muted-foreground text-xs font-medium tracking-wide uppercase\">{{\n                    filter.label\n                  }}</Label>\n                  <Badge\n                    v-if=\"getDateRangeValue(filter.column).from || getDateRangeValue(filter.column).to\"\n                    variant=\"secondary\"\n                    class=\"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                </div>\n                <button\n                  v-if=\"getDateRangeValue(filter.column).from || getDateRangeValue(filter.column).to\"\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                  @click=\"emit('clear-date-filter', filter)\"\n                >\n                  Clear\n                </button>\n              </div>\n              <div :class=\"['flex justify-center overflow-hidden', borderless ? '' : 'rounded-md border']\">\n                <RangeCalendar\n                  :model-value=\"getCalendarModel(filter.column)\"\n                  :number-of-months=\"1\"\n                  class=\"p-2\"\n                  @update:model-value=\"emit('calendar-update', filter.column, $event)\"\n                />\n              </div>\n            </div>\n          </template>\n\n          <!-- Consumer-supplied custom filter UI -->\n          <slot name=\"custom-filters\" />\n        </div>\n      </div>\n\n      <!-- Footer -->\n      <div :class=\"['flex gap-2 px-4 py-3', borderless ? '' : 'border-t']\">\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          class=\"flex-1\"\n          :disabled=\"!isAnyFilterActive\"\n          @click=\"emit('clear-all')\"\n        >\n          <X class=\"size-3.5\" />\n          Reset All\n        </Button>\n        <Button\n          size=\"sm\"\n          class=\"flex-1\"\n          @click=\"emit('apply')\"\n        >\n          <template v-if=\"isServerSide\">\n            Apply Filters\n          </template>\n          <template v-else>\n            Show {{ table.getFilteredRowModel().rows.length }} result{{\n              table.getFilteredRowModel().rows.length !== 1 ? 's' : ''\n            }}\n          </template>\n        </Button>\n      </div>\n    </SheetContent>\n  </Sheet>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/DataTableFilterSheet.vue"
    },
    {
      "path": "packages/registry-vue/components/data-table/DataTablePagination.vue",
      "content": "<script setup lang=\"ts\">\nimport type { Table } from '@tanstack/vue-table'\n\nimport { Button } from '@/components/ui/button'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-vue-next'\n\nwithDefaults(\n  defineProps<{\n    table: Table<any>\n    totalRows: number\n    isServerSide: boolean\n    borderless?: boolean\n  }>(),\n  { borderless: false },\n)\n</script>\n\n<template>\n  <div :class=\"['flex items-center justify-between py-3', borderless ? '' : 'border-t px-4']\">\n    <div class=\"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 class=\"flex items-center gap-6 lg:gap-8\">\n      <div class=\"flex items-center gap-2\">\n        <p class=\"text-sm font-medium whitespace-nowrap\">\n          Rows per page\n        </p>\n        <Select\n          :model-value=\"`${table.getState().pagination.pageSize}`\"\n          @update:model-value=\"\n            (value) => {\n              table.setPageSize(Number(value))\n              table.setPageIndex(0)\n            }\n          \"\n        >\n          <SelectTrigger class=\"h-8 w-[70px]\">\n            <SelectValue :placeholder=\"`${table.getState().pagination.pageSize}`\" />\n          </SelectTrigger>\n          <SelectContent side=\"top\">\n            <SelectItem\n              v-for=\"pageSize in [10, 20, 30, 40, 50]\"\n              :key=\"pageSize\"\n              :value=\"`${pageSize}`\"\n            >\n              {{ pageSize }}\n            </SelectItem>\n          </SelectContent>\n        </Select>\n      </div>\n      <div class=\"flex items-center justify-center text-sm font-medium whitespace-nowrap\">\n        Page {{ table.getState().pagination.pageIndex + 1 }} of\n        {{ table.getPageCount() }}\n      </div>\n      <div class=\"flex items-center gap-1\">\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          class=\"size-8\"\n          :disabled=\"!table.getCanPreviousPage()\"\n          @click=\"table.setPageIndex(0)\"\n        >\n          <ChevronsLeft\n            class=\"size-4\"\n            aria-hidden=\"true\"\n          />\n          <span class=\"sr-only\">First page</span>\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          class=\"size-8\"\n          :disabled=\"!table.getCanPreviousPage()\"\n          @click=\"table.previousPage()\"\n        >\n          <ChevronLeft\n            class=\"size-4\"\n            aria-hidden=\"true\"\n          />\n          <span class=\"sr-only\">Previous page</span>\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          class=\"size-8\"\n          :disabled=\"!table.getCanNextPage()\"\n          @click=\"table.nextPage()\"\n        >\n          <ChevronRight\n            class=\"size-4\"\n            aria-hidden=\"true\"\n          />\n          <span class=\"sr-only\">Next page</span>\n        </Button>\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          class=\"size-8\"\n          :disabled=\"!table.getCanNextPage()\"\n          @click=\"table.setPageIndex(table.getPageCount() - 1)\"\n        >\n          <ChevronsRight\n            class=\"size-4\"\n            aria-hidden=\"true\"\n          />\n          <span class=\"sr-only\">Last page</span>\n        </Button>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/DataTablePagination.vue"
    },
    {
      "path": "packages/registry-vue/components/data-table/DataTableToolbar.vue",
      "content": "<script setup lang=\"ts\">\nimport type { Table } from '@tanstack/vue-table'\nimport type { FilterDefinition, FilterOption } from './DataTable.vue'\n\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 { CalendarIcon, Check, ChevronDown, Download, ListFilter, Plus, Rows3, SlidersHorizontal, X } from 'lucide-vue-next'\nimport DataTableFilterPopover from './DataTableFilterPopover.vue'\n\nfunction resolveOption(opt: string | FilterOption): FilterOption {\n  if (typeof opt === 'string') return { value: opt, label: opt }\n  return opt\n}\n\nwithDefaults(\n  defineProps<{\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) => any\n  }>(),\n  { enableSearch: true, enableColumnVisibility: true, enableExport: false, enableDensityToggle: false, density: 'cozy', borderless: false },\n)\n\nconst emit = defineEmits<{\n  (e: 'search', value: string): void\n  (e: 'open-filter-sheet' | 'clear-all-filters' | 'apply-filters' | 'export-csv' | 'export-json'): void\n  (e: 'toggle-multiselect', column: string, value: string): void\n  (e: 'clear-filter' | 'clear-date-filter', filter: FilterDefinition): void\n  (e: 'calendar-update', column: string, value: any): void\n  (e: 'text-filter-update', column: string, value: string | undefined): void\n  // Popover filter mode: commit-draft fires when the user clicks Apply\n  // in the staged-edit popover. The payload is a Record<columnId, value>\n  // ready to be fed into `setFilterValue` per column.\n  (e: 'commit-filters', draft: Record<string, any>): void\n  (e: 'update:density', value: 'compact' | 'cozy' | 'comfortable'): void\n}>()\n</script>\n\n<template>\n  <div :class=\"['flex flex-col gap-2 py-3', borderless ? '' : 'border-b px-4']\">\n    <div class=\"flex items-center gap-2\">\n      <!-- Search only renders when filterColumn maps to an actual column\n           in the table; otherwise TanStack's getColumn() logs a noisy\n           \"[Table] Column with id '<x>' does not exist\" warning even\n           though our optional-chaining keeps the call safe. -->\n      <Input\n        v-if=\"enableSearch && filterColumn && table.getColumn(filterColumn)\"\n        class=\"h-9 max-w-xs\"\n        :placeholder=\"filterPlaceholder\"\n        :model-value=\"table.getColumn(filterColumn)?.getFilterValue() as string\"\n        @update:model-value=\"emit('search', $event as string)\"\n      />\n\n      <!-- ── INLINE filter mode ── -->\n      <template v-if=\"filterMode === 'inline'\">\n        <template\n          v-for=\"filter in filters\"\n          :key=\"filter.column\"\n        >\n          <!-- Multiselect / Select -->\n          <Popover v-if=\"filter.type === 'multiselect' || filter.type === 'select'\">\n            <PopoverTrigger as-child>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"h-9 border-dashed\"\n              >\n                <Plus\n                  class=\"size-4\"\n                  aria-hidden=\"true\"\n                />\n                {{ filter.label }}\n                <template v-if=\"getMultiSelectValue(filter.column).length > 0\">\n                  <Separator\n                    orientation=\"vertical\"\n                    class=\"mx-1 h-4\"\n                  />\n                  <div class=\"flex gap-1\">\n                    <Badge\n                      v-if=\"getMultiSelectValue(filter.column).length > 2\"\n                      variant=\"secondary\"\n                      class=\"rounded-sm px-1 font-normal\"\n                    >\n                      {{ getMultiSelectValue(filter.column).length }} selected\n                    </Badge>\n                    <template v-else>\n                      <Badge\n                        v-for=\"label in getFilterSelectedLabels(filter)\"\n                        :key=\"label\"\n                        variant=\"secondary\"\n                        class=\"rounded-sm px-1 font-normal\"\n                      >\n                        {{ label }}\n                      </Badge>\n                    </template>\n                  </div>\n                </template>\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent\n              class=\"w-52 p-0\"\n              align=\"start\"\n            >\n              <Command>\n                <CommandInput :placeholder=\"`Search ${filter.label.toLowerCase()}...`\" />\n                <CommandList>\n                  <CommandEmpty>No results.</CommandEmpty>\n                  <CommandGroup>\n                    <CommandItem\n                      v-for=\"rawOpt in filter.options\"\n                      :key=\"resolveOption(rawOpt).value\"\n                      :value=\"resolveOption(rawOpt).label\"\n                      @select=\"emit('toggle-multiselect', filter.column, resolveOption(rawOpt).value)\"\n                    >\n                      <div\n                        class=\"border-primary flex size-4 shrink-0 items-center justify-center rounded-sm border\"\n                        :class=\"[\n                          getMultiSelectValue(filter.column).includes(resolveOption(rawOpt).value)\n                            ? 'bg-primary text-primary-foreground'\n                            : 'opacity-50 [&_svg]:invisible',\n                        ]\"\n                      >\n                        <Check class=\"size-3\" />\n                      </div>\n                      <component\n                        :is=\"resolveOption(rawOpt).icon\"\n                        v-if=\"resolveOption(rawOpt).icon\"\n                        class=\"text-muted-foreground size-4\"\n                      />\n                      <span>{{ resolveOption(rawOpt).label }}</span>\n                    </CommandItem>\n                  </CommandGroup>\n                  <template v-if=\"getMultiSelectValue(filter.column).length > 0\">\n                    <CommandSeparator />\n                    <CommandGroup>\n                      <CommandItem\n                        value=\"__clear__\"\n                        class=\"justify-center text-center\"\n                        @select=\"emit('clear-filter', filter)\"\n                      >\n                        Clear filter\n                      </CommandItem>\n                    </CommandGroup>\n                  </template>\n                </CommandList>\n              </Command>\n            </PopoverContent>\n          </Popover>\n\n          <!-- Date range filter -->\n          <Popover v-else-if=\"filter.type === 'date'\">\n            <PopoverTrigger as-child>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"h-9 border-dashed\"\n              >\n                <CalendarIcon\n                  class=\"size-4\"\n                  aria-hidden=\"true\"\n                />\n                {{ filter.label }}\n                <template v-if=\"getDateRangeValue(filter.column).from || getDateRangeValue(filter.column).to\">\n                  <Separator\n                    orientation=\"vertical\"\n                    class=\"mx-1 h-4\"\n                  />\n                  <Badge\n                    variant=\"secondary\"\n                    class=\"rounded-sm px-1 font-normal\"\n                  >\n                    {{ formatDateRange(filter.column) }}\n                  </Badge>\n                </template>\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent\n              class=\"w-auto p-0\"\n              align=\"start\"\n            >\n              <RangeCalendar\n                :model-value=\"getCalendarModel(filter.column)\"\n                initial-focus\n                :number-of-months=\"2\"\n                @update:model-value=\"emit('calendar-update', filter.column, $event)\"\n              />\n              <div\n                v-if=\"getDateRangeValue(filter.column).from || getDateRangeValue(filter.column).to\"\n                class=\"border-t p-2\"\n              >\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"h-7 w-full text-xs\"\n                  @click=\"emit('clear-date-filter', filter)\"\n                >\n                  Clear dates\n                </Button>\n              </div>\n            </PopoverContent>\n          </Popover>\n\n          <!-- Text filter -->\n          <Popover v-else-if=\"filter.type === 'text'\">\n            <PopoverTrigger as-child>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                class=\"h-9 border-dashed\"\n              >\n                <Plus\n                  class=\"size-4\"\n                  aria-hidden=\"true\"\n                />\n                {{ filter.label }}\n                <template v-if=\"table.getColumn(filter.column)?.getFilterValue() as string\">\n                  <Separator\n                    orientation=\"vertical\"\n                    class=\"mx-1 h-4\"\n                  />\n                  <Badge\n                    variant=\"secondary\"\n                    class=\"rounded-sm px-1 font-normal\"\n                  >\n                    {{ table.getColumn(filter.column)?.getFilterValue() }}\n                  </Badge>\n                </template>\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent\n              class=\"w-60 p-3\"\n              align=\"start\"\n            >\n              <div class=\"space-y-2\">\n                <p class=\"text-sm font-medium\">\n                  {{ filter.label }}\n                </p>\n                <Input\n                  :placeholder=\"`Filter by ${filter.label.toLowerCase()}...`\"\n                  :model-value=\"(table.getColumn(filter.column)?.getFilterValue() as string) ?? ''\"\n                  class=\"h-8 text-sm\"\n                  @update:model-value=\"table.getColumn(filter.column)?.setFilterValue($event || undefined)\"\n                />\n              </div>\n            </PopoverContent>\n          </Popover>\n        </template>\n      </template>\n\n      <!-- ── MODAL filter mode (Sheet from the right) ── -->\n      <template v-if=\"filterMode === 'modal'\">\n        <Button\n          variant=\"outline\"\n          size=\"sm\"\n          class=\"h-9\"\n          :class=\"[activeFilterCount > 0 ? 'border-primary/40 bg-primary/5 text-primary hover:bg-primary/10' : '']\"\n          @click=\"emit('open-filter-sheet')\"\n        >\n          <SlidersHorizontal\n            class=\"size-4\"\n            aria-hidden=\"true\"\n          />\n          Filters\n          <Badge\n            v-if=\"activeFilterCount > 0\"\n            class=\"bg-primary text-primary-foreground ml-0.5 size-5 rounded-full p-0 text-xs font-semibold\"\n          >\n            {{ activeFilterCount }}\n          </Badge>\n        </Button>\n      </template>\n\n      <!-- ── POPOVER filter mode ── -->\n      <template v-if=\"filterMode === 'popover' && filters.length\">\n        <DataTableFilterPopover\n          :table=\"table\"\n          :filters=\"filters\"\n          :active-filter-count=\"activeFilterCount\"\n          :is-any-filter-active=\"isAnyFilterActive\"\n          :is-server-side=\"isServerSide\"\n          :get-multi-select-value=\"getMultiSelectValue\"\n          :get-date-range-value=\"getDateRangeValue\"\n          :format-date-range=\"formatDateRange\"\n          :get-calendar-model=\"getCalendarModel\"\n          @commit-draft=\"(d) => emit('commit-filters', d)\"\n          @clear-all=\"emit('clear-all-filters')\"\n        >\n          <template\n            v-if=\"$slots['custom-filters']\"\n            #custom-filters\n          >\n            <slot name=\"custom-filters\" />\n          </template>\n        </DataTableFilterPopover>\n      </template>\n\n      <!-- Inline custom filters (when filterMode === 'inline') -->\n      <slot\n        v-if=\"filterMode === 'inline'\"\n        name=\"custom-filters\"\n      />\n\n      <!-- Toolbar extras (e.g. group-by selector, density toggle) -->\n      <slot name=\"toolbar-extra\" />\n\n      <!-- Reset button -->\n      <Button\n        v-if=\"isAnyFilterActive\"\n        variant=\"ghost\"\n        size=\"sm\"\n        class=\"h-9\"\n        @click=\"emit('clear-all-filters')\"\n      >\n        Reset\n        <X\n          class=\"size-4\"\n          aria-hidden=\"true\"\n        />\n      </Button>\n\n      <!-- Export (CSV / JSON) -->\n      <div\n        v-if=\"enableExport\"\n        class=\"ml-auto\"\n      >\n        <DropdownMenu>\n          <DropdownMenuTrigger as-child>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-9\"\n            >\n              <Download class=\"size-3.5\" />\n              Export\n              <ChevronDown class=\"size-3.5 opacity-60\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuItem @select=\"emit('export-csv')\">\n              Export as CSV\n            </DropdownMenuItem>\n            <DropdownMenuItem @select=\"emit('export-json')\">\n              Export as JSON\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <!-- Density toggle -->\n      <div\n        v-if=\"enableDensityToggle\"\n        :class=\"enableExport ? '' : 'ml-auto'\"\n      >\n        <DropdownMenu>\n          <DropdownMenuTrigger as-child>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-9\"\n              :aria-label=\"`Row density: ${density}`\"\n            >\n              <Rows3\n                class=\"size-4\"\n                aria-hidden=\"true\"\n              />\n              <span class=\"capitalize\">{{ density }}</span>\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuRadioGroup\n              :model-value=\"density\"\n              @update:model-value=\"(v: any) => emit('update:density', v as 'compact' | 'cozy' | 'comfortable')\"\n            >\n              <DropdownMenuRadioItem value=\"compact\">\n                Compact\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"cozy\">\n                Cozy\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"comfortable\">\n                Comfortable\n              </DropdownMenuRadioItem>\n            </DropdownMenuRadioGroup>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <!-- Column Visibility -->\n      <div\n        v-if=\"enableColumnVisibility\"\n        :class=\"enableExport || enableDensityToggle ? '' : 'ml-auto'\"\n      >\n        <DropdownMenu>\n          <DropdownMenuTrigger as-child>\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-9\"\n            >\n              <ListFilter\n                class=\"size-4\"\n                aria-hidden=\"true\"\n              />\n              View\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            <DropdownMenuCheckboxItem\n              v-for=\"column in table.getAllColumns().filter((column) => column.getCanHide())\"\n              :key=\"column.id\"\n              class=\"capitalize\"\n              :checked=\"column.getIsVisible()\"\n              @update:checked=\"(value: boolean) => column.toggleVisibility(!!value)\"\n            >\n              {{ column.id }}\n            </DropdownMenuCheckboxItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/DataTableToolbar.vue"
    },
    {
      "path": "packages/registry-vue/components/data-table/index.ts",
      "content": "export { default as DataTable } from './DataTable.vue'\nexport { default as DataTableColumnHeader } from './DataTableColumnHeader.vue'\nexport { default as DataTableToolbar } from './DataTableToolbar.vue'\nexport { default as DataTableFilterSheet } from './DataTableFilterSheet.vue'\nexport { default as DataTableFilterPopover } from './DataTableFilterPopover.vue'\nexport { default as DataTablePagination } from './DataTablePagination.vue'\nexport type { FilterDefinition, FilterOption } from './DataTable.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/data-table/index.ts"
    }
  ],
  "dependencies": [
    "@internationalized/date",
    "@tanstack/vue-table",
    "lucide-vue-next",
    "reka-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/command.json",
    "https://uipkge.dev/r/vue/dropdown-menu.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/label.json",
    "https://uipkge.dev/r/vue/popover.json",
    "https://uipkge.dev/r/vue/range-calendar.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/sheet.json",
    "https://uipkge.dev/r/vue/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"
  ]
}