{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/data-table/DataTable.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { ColumnDef, Row, Table as TanstackTable } from '@tanstack/table-core'\n  import type { FilterDefinition, FilterOption } from './types'\n\n  export type { FilterDefinition, FilterOption }\n\n  export interface DataTableState {\n    page: number\n    pageSize: number\n    sortBy: string\n    sortOrder: 'asc' | 'desc'\n    filters: Record<string, any>\n    search: string\n  }\n\n  export interface DataTableProps<TData, TValue> {\n    columns: ColumnDef<TData, TValue>[]\n    data: TData[]\n    filterColumn?: string\n    filterPlaceholder?: string\n    filters?: FilterDefinition[]\n    filterMode?: 'inline' | 'modal' | 'popover'\n    /** Show the global search input. Default true. */\n    enableSearch?: boolean\n    /** Show the View column-visibility dropdown. Default false. */\n    enableColumnVisibility?: boolean\n    /** Show the pagination footer. Default true. */\n    enablePagination?: boolean\n    /** Hide the entire toolbar (search + filters + view). Default false. */\n    hideToolbar?: boolean\n    /** Show Export-CSV button in toolbar. Default false. */\n    enableExport?: boolean\n    /** Infinite-scroll mode: calls `onFetchMore` when last row enters viewport,\n     *  hides the pagination footer. Combine with append-on-success on the\n     *  consumer side. */\n    infinite?: boolean\n    /** Allow drag-to-resize on column borders. */\n    enableResize?: boolean\n    /** Initial column pinning. Each column id is pinned to the given side. */\n    defaultColumnPinning?: { left?: string[]; right?: string[] }\n    /** Allow drag-to-reorder on column headers. */\n    enableReorder?: boolean\n    /** Initial column ids to group by. Pass an empty array to disable grouping. */\n    defaultGrouping?: string[]\n    /** Virtual-scrolling mode (CSS content-visibility based). Best with large\n     *  datasets and `maxHeight` for a scroll container. */\n    virtual?: boolean\n    /** Sticky header — keeps `<thead>` visible when scrolling. Pair with `maxHeight`. */\n    stickyHeader?: boolean\n    /** Density of cell padding: 'compact' | 'cozy' | 'comfortable'. Default cozy.\n     *  Treated as the INITIAL density when `enableDensityToggle` is on; the user\n     *  can override it at runtime from the toolbar. */\n    density?: 'compact' | 'cozy' | 'comfortable'\n    /** Show the density toggle in the toolbar. Default false. */\n    enableDensityToggle?: boolean\n    /** Strip the DataTable's borders.\n     *  - `'inner'` removes toolbar bottom-divider, pagination top-divider,\n     *    and filter-sheet section bgs/borders. The outer container border\n     *    is kept.\n     *  - `'full'` additionally removes the outer container border + rounded\n     *    corners so the table renders completely flat on the canvas. */\n    borderless?: 'inner' | 'full'\n    /** Where the toolbar renders. `'inside'` (default) lives inside the\n     *  bordered container; `'above'` floats outside it. */\n    toolbarPosition?: 'inside' | 'above'\n    /** Where the pagination footer renders. `'inside'` (default) or `'below'`. */\n    paginationPosition?: 'inside' | 'below'\n    /** Max height; enables vertical scroll inside the card. */\n    maxHeight?: string\n    /** Row click — when set, rows become clickable + cursor-pointer. */\n    onRowClick?: (row: TData) => void\n    /** Server-side mode: total row count from API (enables manual pagination) */\n    totalRows?: number\n    /** Server-side mode: loading state indicator */\n    loading?: boolean\n    /** Emitted when server-side state changes (pagination, sorting, filters) */\n    onStateChange?: (state: DataTableState) => void\n    /** Infinite-scroll: last row entered viewport, time to load more */\n    onFetchMore?: () => void\n    /** Custom empty-state content (replaces \"No results.\"). */\n    emptyState?: Snippet\n    /** Expanded-row content. Receives the original row + the TanStack row. */\n    renderExpanded?: Snippet<[TData, Row<TData>]>\n    /** Bulk action bar content (shown above the table when rows are selected). */\n    renderBulkActions?: Snippet<[Row<TData>[], () => void]>\n    /** Footer (<tfoot>) content. */\n    renderFooter?: Snippet<[Row<TData>[]]>\n    /** Extra toolbar controls (e.g. group-by selector). */\n    toolbarExtra?: Snippet\n    /** Consumer-supplied custom filter UI inside the filter surface. */\n    customFilters?: Snippet\n    /** Enable keyboard navigation (Arrow keys, J/K, Space/X, Enter, Esc, Cmd+A, Cmd+C). Default true. */\n    enableKeyboardNavigation?: boolean\n    /** Placement of the bulk actions dock: 'floating' (Linear/Raycast HUD dock at bottom-center) or 'inline' (top banner). Default 'floating'. */\n    bulkActionPosition?: 'floating' | 'inline'\n  }\n\n  /** Instance API, via `bind:this` (the Svelte twin of React's forwarded ref). */\n  export interface DataTableHandle<TData> {\n    table: TanstackTable<TData>\n    exportCsv: () => void\n    exportJson: () => void\n    copyTsv: (rows?: Row<TData>[]) => string\n    copyMarkdown: (rows?: Row<TData>[]) => string\n  }\n</script>\n\n<script lang=\"ts\" generics=\"TData, TValue\">\n  import type {\n    Column,\n    ColumnFiltersState,\n    ColumnPinningState,\n    ExpandedState,\n    FilterFn,\n    GroupingState,\n    Header,\n    RowSelectionState,\n    SortingState,\n    VisibilityState,\n  } from '@tanstack/table-core'\n  import {\n    getCoreRowModel,\n    getExpandedRowModel,\n    getFilteredRowModel,\n    getGroupedRowModel,\n    getPaginationRowModel,\n    getSortedRowModel,\n  } from '@tanstack/table-core'\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.\n  import {\n    Table,\n    TableBody,\n    TableCell,\n    TableHead,\n    TableHeader,\n    TableRow,\n    valueUpdater,\n  } from '$lib/components/ui/table'\n  import type { DateRange } from '$lib/components/ui/range-calendar'\n  import { ChevronDown, ChevronRight, Copy, Download, Search, X } from '@lucide/svelte'\n\n  import DataTableToolbar from './DataTableToolbar.svelte'\n  import DataTableFilterSheet from './DataTableFilterSheet.svelte'\n  import DataTablePagination from './DataTablePagination.svelte'\n  import FlexRender from './FlexRender.svelte'\n  import { createSvelteTable } from './table.svelte'\n  import { resolveOption } from './types'\n  import { dateToIso, isoRangeToCalendar } from './date-utils'\n\n  interface DateRangeValue {\n    from?: string\n    to?: string\n  }\n\n  let {\n    columns,\n    data,\n    filterColumn = '',\n    filterPlaceholder = 'Filter...',\n    filters = [],\n    // Industry default (shadcn / Linear / Airtable): faceted filter chips in the\n    // toolbar. Use `modal` for a right Sheet when you have many complex filters.\n    filterMode = 'inline',\n    enableSearch = true,\n    enableColumnVisibility = false,\n    enablePagination = true,\n    hideToolbar = false,\n    enableExport = false,\n    infinite = false,\n    enableResize = false,\n    defaultColumnPinning = { left: [], right: [] },\n    enableReorder = false,\n    defaultGrouping = [],\n    virtual = false,\n    stickyHeader = false,\n    density = 'cozy',\n    enableDensityToggle = false,\n    borderless,\n    toolbarPosition = 'inside',\n    paginationPosition = 'inside',\n    maxHeight = '',\n    onRowClick,\n    totalRows = -1,\n    loading = false,\n    onStateChange,\n    onFetchMore,\n    emptyState,\n    renderExpanded,\n    renderBulkActions,\n    renderFooter,\n    toolbarExtra,\n    customFilters,\n    enableKeyboardNavigation = true,\n    bulkActionPosition = 'floating',\n  }: DataTableProps<TData, TValue> = $props()\n\n  // User-mutable density -- initial value from prop, toggleable via toolbar\n  // when `enableDensityToggle` is on.\n  let densityOverride = $state<'compact' | 'cozy' | 'comfortable' | null>(null)\n  const currentDensity = $derived(densityOverride ?? density)\n\n  // `borderless` is an enum (`'inner'` vs `'full'`); children only need a\n  // boolean \"should I drop my borders?\".\n  const dropInnerBorders = $derived(!!borderless)\n\n  const densityClass = $derived(\n    currentDensity === 'compact'\n      ? '[&_td]:py-2 [&_td]:px-3 [&_td]:text-xs [&_th]:h-8 [&_th]:px-3 [&_th]:text-xs'\n      : currentDensity === 'comfortable'\n        ? '[&_td]:py-3 [&_th]:h-12'\n        : // cozy -- tightens TableCell/TableHead defaults (py-3 / h-12)\n          '[&_td]:py-2 [&_th]:h-10',\n  )\n\n  const isServerSide = $derived(totalRows >= 0)\n  let isFilterSheetOpen = $state(false)\n\n  // ── Filter snapshot (restore on close without apply) ───────────────────\n  let filterSnapshot: ColumnFiltersState | null = null\n  let dateRangeSnapshot: Record<string, DateRangeValue> | null = null\n  let filterApplied = false\n\n  // ── Timers ──────────────────────────────────────────────────────────────\n  let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null\n  let paginationEmitTimer: ReturnType<typeof setTimeout> | null = null\n  let isMounted = false\n\n  $effect(() => {\n    isMounted = true\n    return () => {\n      isMounted = false\n      if (searchDebounceTimer) clearTimeout(searchDebounceTimer)\n      if (paginationEmitTimer) clearTimeout(paginationEmitTimer)\n    }\n  })\n\n  // Custom filter functions for multiselect and date range\n  const multiSelectFilterFn: FilterFn<TData> = (row, columnId, filterValue: string[]) => {\n    if (!filterValue || filterValue.length === 0) return true\n    const cellValue = String(row.getValue(columnId)).toLowerCase()\n    return filterValue.some((v: string) => v.toLowerCase() === cellValue)\n  }\n\n  const dateRangeFilterFn: FilterFn<TData> = (row, columnId, filterValue: DateRangeValue) => {\n    if (!filterValue) return true\n    const { from, to } = filterValue\n    if (!from && !to) return true\n    const cellValue = String(row.getValue(columnId))\n    if (from && cellValue < from) return false\n    if (to && cellValue > to) return false\n    return true\n  }\n\n  // Augment columns with custom filter functions based on filter definitions\n  const processedColumns = $derived(\n    columns.map((col) => {\n      const colId = (col as any).accessorKey || (col as any).id\n      const filter = filters.find((f) => f.column === colId)\n      if (!filter) return col\n      if (filter.type === 'multiselect') return { ...col, filterFn: multiSelectFilterFn }\n      if (filter.type === 'date') return { ...col, filterFn: dateRangeFilterFn }\n      return col\n    }),\n  )\n\n  // Table state slices are replaced immutably by TanStack updaters, so raw\n  // state (no deep proxy) is enough and keeps reference-equality memos cheap.\n  let sorting = $state.raw<SortingState>([])\n  let columnFilters = $state.raw<ColumnFiltersState>([])\n  let columnVisibility = $state.raw<VisibilityState>({})\n  let rowSelection = $state.raw<RowSelectionState>({})\n  let expanded = $state.raw<ExpandedState>({})\n  // svelte-ignore state_referenced_locally: pinning / grouping props are initial values by design.\n  let columnPinning = $state.raw<ColumnPinningState>({\n    left: defaultColumnPinning.left ?? [],\n    right: defaultColumnPinning.right ?? [],\n  })\n  let columnOrder = $state.raw<string[]>([])\n  // svelte-ignore state_referenced_locally: initial grouping only.\n  let grouping = $state.raw<GroupingState>(defaultGrouping)\n  let pagination = $state.raw({ pageIndex: 0, pageSize: 10 })\n\n  // Per-filter reactive range calendar model (ISO strings), keyed by column.\n  let dateRangeModels = $state.raw<Record<string, DateRangeValue>>({})\n\n  /** `{ value }` box over a state slice so the shared `valueUpdater` can drive it. */\n  function box<T>(get: () => T, set: (v: T) => void): { value: T } {\n    return {\n      get value() {\n        return get()\n      },\n      set value(v: T) {\n        set(v)\n      },\n    }\n  }\n\n  const paginationRowModel = getPaginationRowModel<TData>()\n  const sortedRowModel = getSortedRowModel<TData>()\n  const filteredRowModel = getFilteredRowModel<TData>()\n\n  export const table: TanstackTable<TData> = createSvelteTable<TData>({\n    get data() {\n      return data\n    },\n    get columns() {\n      return processedColumns\n    },\n    getCoreRowModel: getCoreRowModel(),\n    // Skip client pagination entirely when disabled (not just hide the footer UI).\n    get getPaginationRowModel() {\n      return isServerSide || !enablePagination ? undefined : paginationRowModel\n    },\n    get getSortedRowModel() {\n      return isServerSide ? undefined : sortedRowModel\n    },\n    get getFilteredRowModel() {\n      return isServerSide ? undefined : filteredRowModel\n    },\n    getExpandedRowModel: getExpandedRowModel(),\n    getGroupedRowModel: getGroupedRowModel(),\n    get enableColumnResizing() {\n      return enableResize\n    },\n    columnResizeMode: 'onChange',\n    get manualPagination() {\n      return isServerSide\n    },\n    get manualSorting() {\n      return isServerSide\n    },\n    get manualFiltering() {\n      return isServerSide\n    },\n    get rowCount() {\n      return isServerSide ? totalRows : undefined\n    },\n    onSortingChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => sorting, (v) => (sorting = v)))\n      if (isServerSide) {\n        setTimeout(() => emitStateUpdate(), 0)\n      }\n    },\n    onColumnFiltersChange: (updaterOrValue) => {\n      // Server-side: never auto-emit on filter change — handled by Apply / search debounce\n      // Client-side inline: filters apply locally via TanStack, no emit needed\n      valueUpdater(updaterOrValue, box(() => columnFilters, (v) => (columnFilters = v)))\n    },\n    onColumnVisibilityChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => columnVisibility, (v) => (columnVisibility = v)))\n    },\n    onRowSelectionChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => rowSelection, (v) => (rowSelection = v)))\n    },\n    onExpandedChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => expanded, (v) => (expanded = v)))\n    },\n    onPaginationChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => pagination, (v) => (pagination = v)))\n      if (isServerSide) {\n        if (paginationEmitTimer) clearTimeout(paginationEmitTimer)\n        paginationEmitTimer = setTimeout(() => emitStateUpdate(), 0)\n      }\n    },\n    onColumnPinningChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => columnPinning, (v) => (columnPinning = v)))\n    },\n    onColumnOrderChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => columnOrder, (v) => (columnOrder = v)))\n    },\n    onGroupingChange: (updaterOrValue) => {\n      valueUpdater(updaterOrValue, box(() => grouping, (v) => (grouping = v)))\n    },\n    state: {\n      get sorting() {\n        return sorting\n      },\n      get columnFilters() {\n        return columnFilters\n      },\n      get columnVisibility() {\n        return columnVisibility\n      },\n      get pagination() {\n        return pagination\n      },\n      get rowSelection() {\n        return rowSelection\n      },\n      get expanded() {\n        return expanded\n      },\n      get columnPinning() {\n        return columnPinning\n      },\n      get columnOrder() {\n        return columnOrder\n      },\n      get grouping() {\n        return grouping\n      },\n    },\n  })\n\n  /** Build and emit current server-side state */\n  function emitStateUpdate() {\n    if (!isMounted) return\n    const s = sorting[0]\n    const filterMap: Record<string, any> = {}\n    for (const cf of columnFilters) {\n      filterMap[cf.id] = cf.value\n    }\n    onStateChange?.({\n      page: table.getState().pagination.pageIndex + 1,\n      pageSize: table.getState().pagination.pageSize,\n      sortBy: s?.id || '',\n      sortOrder: s?.desc ? 'desc' : 'asc',\n      filters: filterMap,\n      search: (filterColumn && (table.getColumn(filterColumn)?.getFilterValue() as string)) || '',\n    })\n  }\n\n  function onSearchInput(val: string) {\n    if (!filterColumn || !table.getColumn(filterColumn)) return\n    table.getColumn(filterColumn)?.setFilterValue(val || undefined)\n    if (isServerSide) {\n      if (searchDebounceTimer) clearTimeout(searchDebounceTimer)\n      searchDebounceTimer = setTimeout(() => {\n        table.setPageIndex(0)\n        emitStateUpdate()\n      }, 500)\n    }\n  }\n\n  // ── Filter snapshot helpers ──────────────────────────────────────────────\n  function snapshotFilters() {\n    filterApplied = false\n    filterSnapshot = JSON.parse(JSON.stringify(columnFilters))\n    dateRangeSnapshot = JSON.parse(JSON.stringify(dateRangeModels))\n  }\n\n  function maybeRestoreFilters() {\n    if (!filterApplied && filterSnapshot) {\n      columnFilters = filterSnapshot\n      dateRangeModels = dateRangeSnapshot ?? {}\n    }\n    filterSnapshot = null\n    dateRangeSnapshot = null\n  }\n\n  function onFilterSheetOpen() {\n    snapshotFilters()\n    isFilterSheetOpen = true\n  }\n\n  function onFilterSheetClose(open: boolean) {\n    if (!open) maybeRestoreFilters()\n    isFilterSheetOpen = open\n  }\n\n  // ── Filter helpers ─────────────────────────────────────────────────────\n  function getMultiSelectValue(column: string): string[] {\n    return (table.getColumn(column)?.getFilterValue() as string[]) ?? []\n  }\n\n  function getDateRangeValue(column: string): DateRangeValue {\n    return (table.getColumn(column)?.getFilterValue() as DateRangeValue) ?? {}\n  }\n\n  function getCalendarModel(column: string): DateRange | undefined {\n    return isoRangeToCalendar(getDateRangeValue(column))\n  }\n\n  function onCalendarUpdate(column: string, val: DateRange | undefined) {\n    const from = val?.start ? dateToIso(val.start) : undefined\n    const to = val?.end ? dateToIso(val.end) : undefined\n    dateRangeModels = { ...dateRangeModels, [column]: { from, to } }\n    const hasValue = from || to\n    table.getColumn(column)?.setFilterValue(hasValue ? { from, to } : undefined)\n  }\n\n  function formatDateRange(column: string): string {\n    const dr = getDateRangeValue(column)\n    if (dr.from && dr.to) return `${dr.from} - ${dr.to}`\n    if (dr.from) return `From ${dr.from}`\n    if (dr.to) return `Until ${dr.to}`\n    return ''\n  }\n\n  function toggleMultiSelectValue(column: string, option: string) {\n    const current = getMultiSelectValue(column)\n    const next = current.includes(option) ? current.filter((v) => v !== option) : [...current, option]\n    table.getColumn(column)?.setFilterValue(next.length > 0 ? next : undefined)\n  }\n\n  function clearAllFilters() {\n    filterApplied = true\n    if (searchDebounceTimer) clearTimeout(searchDebounceTimer)\n    filters.forEach((f) => {\n      table.getColumn(f.column)?.setFilterValue(undefined)\n      if (f.type === 'date') {\n        dateRangeModels = { ...dateRangeModels, [f.column]: {} }\n      }\n    })\n    if (filterColumn && table.getColumn(filterColumn)) {\n      table.getColumn(filterColumn)?.setFilterValue(undefined)\n    }\n    if (isServerSide) {\n      table.setPageIndex(0)\n      queueMicrotask(() => emitStateUpdate())\n    }\n  }\n\n  function isFilterActive(filter: FilterDefinition): boolean {\n    const value = table.getColumn(filter.column)?.getFilterValue()\n    if (value === undefined || value === '') return false\n    if (filter.type === 'multiselect') return Array.isArray(value) && value.length > 0\n    if (filter.type === 'date') {\n      const d = value as DateRangeValue\n      return !!(d.from || d.to)\n    }\n    return true\n  }\n\n  const hasSearchValue = $derived(!!(filterColumn && (table.getColumn(filterColumn)?.getFilterValue() as string)))\n  const isAnyFilterActive = $derived(hasSearchValue || filters.some(isFilterActive))\n\n  function getFilterSelectedLabels(filter: FilterDefinition): string[] {\n    const vals = getMultiSelectValue(filter.column)\n    return vals.map((v) => {\n      const opt = filter.options?.find((o) => resolveOption(o).value === v)\n      return opt ? resolveOption(opt).label : v\n    })\n  }\n\n  function clearFilter(filter: FilterDefinition) {\n    table.getColumn(filter.column)?.setFilterValue(undefined)\n  }\n\n  const activeFilterCount = $derived(filters.filter(isFilterActive).length)\n\n  function clearDateFilter(filter: FilterDefinition) {\n    clearFilter(filter)\n    dateRangeModels = { ...dateRangeModels, [filter.column]: {} }\n  }\n\n  function onApplyFilters() {\n    filterApplied = true\n    if (isServerSide) {\n      if (table.getState().pagination.pageIndex === 0) {\n        emitStateUpdate()\n      } else {\n        table.setPageIndex(0)\n      }\n    }\n    isFilterSheetOpen = false\n  }\n\n  /**\n   * Popover filter mode: handle the staged-edit commit. Walk the draft,\n   * write each column's value via `setFilterValue`, and sync the calendar\n   * model for date filters so a subsequent reopen reflects the just-applied\n   * range.\n   */\n  function onCommitDraft(draft: Record<string, any>) {\n    for (const f of filters) {\n      if (!(f.column in draft)) continue\n      const val = draft[f.column]\n      if (f.type === 'date') {\n        const dr = (val ?? {}) as DateRangeValue\n        dateRangeModels = { ...dateRangeModels, [f.column]: { from: dr.from, to: dr.to } }\n        table.getColumn(f.column)?.setFilterValue(val)\n      } else {\n        table.getColumn(f.column)?.setFilterValue(val)\n      }\n    }\n    onApplyFilters()\n  }\n\n  // ── Column reorder (HTML5 drag/drop swaps dragged column with drop target) ──\n  let dragColId = $state<string | null>(null)\n  let dragOverColId = $state<string | null>(null)\n\n  function onColDragStart(id: string, e: DragEvent) {\n    dragColId = id\n    document.body.style.cursor = 'grabbing'\n    if (e.dataTransfer) {\n      e.dataTransfer.effectAllowed = 'move'\n      e.dataTransfer.setData('text/plain', id)\n    }\n  }\n\n  function onColDragOver(targetId: string, e: DragEvent) {\n    if (!enableReorder || !dragColId) return\n    e.preventDefault()\n    dragOverColId = targetId\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n  }\n\n  function onColDragEnd() {\n    document.body.style.cursor = ''\n    dragColId = null\n    dragOverColId = null\n  }\n\n  function onColDrop(targetId: string) {\n    const src = dragColId\n    onColDragEnd()\n    if (!src || src === targetId) return\n    const order = (columnOrder.length ? columnOrder : table.getAllLeafColumns().map((c) => c.id)).slice()\n    const from = order.indexOf(src)\n    const to = order.indexOf(targetId)\n    if (from === -1 || to === -1) return\n    order.splice(to, 0, ...order.splice(from, 1))\n    columnOrder = order\n  }\n\n  // ── Pinning — return style for sticky pinned cells (header or body) ──\n  function pinStyle(col: Column<TData, unknown>): string | undefined {\n    const side = col.getIsPinned()\n    if (!side) return undefined\n    if (side === 'left') return `position: sticky; left: ${col.getStart('left')}px; z-index: 2`\n    return `position: sticky; right: ${col.getAfter('right')}px; z-index: 2`\n  }\n\n  function headStyle(header: Header<TData, unknown>): string | undefined {\n    const parts: string[] = []\n    if (enableResize) parts.push(`width: ${header.getSize()}px`)\n    const pin = pinStyle(header.column)\n    if (pin) parts.push(pin)\n    return parts.length ? parts.join('; ') : undefined\n  }\n\n  // ── Infinite scroll — sentinel row triggers fetch-more when visible ──\n  // Re-attaches when the sentinel mounts, `infinite` flips, or rows / the\n  // callback change (so a still-visible sentinel re-fires after an append).\n  let sentinelEl = $state<HTMLDivElement | null>(null)\n  $effect(() => {\n    const node = sentinelEl\n    const fetchMore = onFetchMore\n    void data\n    if (!infinite || !node) return\n    const io = new IntersectionObserver(\n      (entries) => {\n        if (entries[0]?.isIntersecting) fetchMore?.()\n      },\n      { rootMargin: '100px' },\n    )\n    io.observe(node)\n    return () => io.disconnect()\n  })\n\n  function exportableColumns() {\n    return table.getVisibleLeafColumns().filter((c) => c.id !== 'select' && c.id !== 'actions' && c.id !== 'expander')\n  }\n\n  function headerLabels(cols: Column<TData, unknown>[]) {\n    return cols.map((c) =>\n      String(c.columnDef.header && typeof c.columnDef.header === 'string' ? c.columnDef.header : c.id),\n    )\n  }\n\n  // ── CSV export — currently filtered + visible data, skips select/actions/expander cols. ──\n  export function exportCsv() {\n    const visibleCols = exportableColumns()\n    const headers = headerLabels(visibleCols)\n    const rows = table.getFilteredRowModel().rows.map((row) =>\n      visibleCols.map((c) => {\n        const v = row.getValue(c.id)\n        const s = v == null ? '' : String(v)\n        return /[\",\\n]/.test(s) ? `\"${s.replace(/\"/g, '\"\"')}\"` : s\n      }),\n    )\n    const csv = [headers.join(','), ...rows.map((r) => r.join(','))].join('\\n')\n    downloadBlob(csv, 'text/csv;charset=utf-8;', `export-${Date.now()}.csv`)\n  }\n\n  // ── JSON export — same scope as CSV, but emits underlying row.original objects ──\n  export function exportJson() {\n    const visibleColIds = exportableColumns().map((c) => c.id)\n    const rows = table.getFilteredRowModel().rows.map((row) => {\n      const original = row.original as Record<string, unknown>\n      const out: Record<string, unknown> = {}\n      for (const id of visibleColIds) {\n        out[id] = id in original ? original[id] : row.getValue(id)\n      }\n      return out\n    })\n    const json = JSON.stringify(rows, null, 2)\n    downloadBlob(json, 'application/json;charset=utf-8;', `export-${Date.now()}.json`)\n  }\n\n  function downloadBlob(content: string, type: string, filename: string) {\n    const blob = new Blob([content], { type })\n    const url = URL.createObjectURL(blob)\n    const a = document.createElement('a')\n    a.href = url\n    a.download = filename\n    document.body.appendChild(a)\n    a.click()\n    document.body.removeChild(a)\n    URL.revokeObjectURL(url)\n  }\n\n  function targetRowsFor(rowsToCopy?: Row<TData>[]) {\n    return rowsToCopy && rowsToCopy.length > 0\n      ? rowsToCopy\n      : table.getSelectedRowModel().rows.length > 0\n        ? table.getSelectedRowModel().rows\n        : table.getFilteredRowModel().rows\n  }\n\n  // ── TSV clipboard copy — formatted for instant paste into Excel / Sheets / Notion ──\n  export function copyTsv(rowsToCopy?: Row<TData>[]) {\n    const visibleCols = exportableColumns()\n    const headers = headerLabels(visibleCols)\n    const rowData = targetRowsFor(rowsToCopy).map((row) =>\n      visibleCols.map((c) => {\n        const v = row.getValue(c.id)\n        return v == null ? '' : String(v).replace(/[\\t\\n\\r]/g, ' ')\n      }),\n    )\n    const tsv = [headers.join('\\t'), ...rowData.map((r) => r.join('\\t'))].join('\\n')\n    if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {\n      navigator.clipboard.writeText(tsv).catch(() => {})\n    }\n    return tsv\n  }\n\n  // ── Markdown table copy — formatted for PR descriptions, issues, and docs ──\n  export function copyMarkdown(rowsToCopy?: Row<TData>[]) {\n    const visibleCols = exportableColumns()\n    const headers = headerLabels(visibleCols)\n    const rowData = targetRowsFor(rowsToCopy).map((row) =>\n      visibleCols.map((c) => {\n        const v = row.getValue(c.id)\n        return v == null ? '' : String(v).replace(/[|\\n\\r]/g, ' ')\n      }),\n    )\n    const sep = visibleCols.map(() => '---')\n    const md = [`| ${headers.join(' | ')} |`, `| ${sep.join(' | ')} |`, ...rowData.map((r) => `| ${r.join(' | ')} |`)].join(\n      '\\n',\n    )\n    if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {\n      navigator.clipboard.writeText(md).catch(() => {})\n    }\n    return md\n  }\n\n  // ── Keyboard ergonomics & focused row tracking (Linear / Raycast craft) ───────\n  let focusedRowIndex = $state(-1)\n  let lastSelectedRowIndex = $state(-1)\n\n  function onTableKeyDown(e: KeyboardEvent) {\n    if (!enableKeyboardNavigation) return\n    const target = e.target as HTMLElement | null\n    if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {\n      return\n    }\n\n    const rows = table.getRowModel().rows\n    if (!rows || rows.length === 0) return\n\n    const isDown = e.key === 'ArrowDown' || e.key === 'j'\n    const isUp = e.key === 'ArrowUp' || e.key === 'k'\n    const isSpace = e.key === ' ' || e.key === 'x'\n    const isEnter = e.key === 'Enter'\n    const isEscape = e.key === 'Escape'\n    const isSelectAll = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'a'\n    const isCopy = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'c'\n\n    if (isDown) {\n      e.preventDefault()\n      const nextIdx = Math.min(focusedRowIndex + 1, rows.length - 1)\n      if (e.shiftKey && focusedRowIndex >= 0) {\n        const start = Math.min(focusedRowIndex, nextIdx)\n        const end = Math.max(focusedRowIndex, nextIdx)\n        for (let i = start; i <= end; i++) {\n          rows[i]?.toggleSelected(true)\n        }\n      }\n      focusedRowIndex = nextIdx\n    } else if (isUp) {\n      e.preventDefault()\n      const prevIdx = Math.max(focusedRowIndex - 1, 0)\n      if (e.shiftKey && focusedRowIndex >= 0) {\n        const start = Math.min(focusedRowIndex, prevIdx)\n        const end = Math.max(focusedRowIndex, prevIdx)\n        for (let i = start; i <= end; i++) {\n          rows[i]?.toggleSelected(true)\n        }\n      }\n      focusedRowIndex = prevIdx\n    } else if (isSpace) {\n      if (focusedRowIndex >= 0 && focusedRowIndex < rows.length) {\n        e.preventDefault()\n        rows[focusedRowIndex]!.toggleSelected()\n        lastSelectedRowIndex = focusedRowIndex\n      }\n    } else if (isEnter) {\n      if (focusedRowIndex >= 0 && focusedRowIndex < rows.length) {\n        e.preventDefault()\n        onRowClick?.(rows[focusedRowIndex]!.original)\n      }\n    } else if (isEscape) {\n      if (table.getSelectedRowModel().rows.length > 0) {\n        e.preventDefault()\n        table.toggleAllRowsSelected(false)\n      }\n      focusedRowIndex = -1\n    } else if (isSelectAll) {\n      e.preventDefault()\n      table.toggleAllRowsSelected(true)\n    } else if (isCopy) {\n      const selected = table.getSelectedRowModel().rows\n      if (selected.length > 0) {\n        e.preventDefault()\n        copyTsv(selected)\n      } else if (focusedRowIndex >= 0 && focusedRowIndex < rows.length) {\n        e.preventDefault()\n        copyTsv([rows[focusedRowIndex]!])\n      }\n    }\n  }\n\n  function onRowClicked(row: Row<TData>, idx: number, event: MouseEvent) {\n    focusedRowIndex = idx\n    if (event.shiftKey && lastSelectedRowIndex >= 0) {\n      const rows = table.getRowModel().rows\n      const start = Math.min(lastSelectedRowIndex, idx)\n      const end = Math.max(lastSelectedRowIndex, idx)\n      for (let i = start; i <= end; i++) {\n        rows[i]?.toggleSelected(true)\n      }\n    } else {\n      lastSelectedRowIndex = idx\n    }\n    onRowClick?.(row.original)\n  }\n\n  const selectedRows = $derived(table.getSelectedRowModel().rows)\n  const bodyRows = $derived(table.getRowModel().rows)\n\n  const toolbarProps = $derived({\n    table: table as TanstackTable<any>,\n    filterColumn,\n    filterPlaceholder,\n    filters,\n    filterMode,\n    enableSearch,\n    enableColumnVisibility,\n    enableExport,\n    enableDensityToggle,\n    density: currentDensity,\n    activeFilterCount,\n    isAnyFilterActive,\n    isServerSide,\n    getMultiSelectValue,\n    getDateRangeValue,\n    getFilterSelectedLabels,\n    formatDateRange,\n    getCalendarModel,\n    onSearch: onSearchInput,\n    onOpenFilterSheet: onFilterSheetOpen,\n    onApplyFilters,\n    onClearAllFilters: clearAllFilters,\n    onToggleMultiselect: toggleMultiSelectValue,\n    onClearFilter: clearFilter,\n    onClearDateFilter: clearDateFilter,\n    onCalendarUpdate,\n    onTextFilterUpdate: (col: string, val: string | undefined) => table.getColumn(col)?.setFilterValue(val),\n    onCommitFilters: onCommitDraft,\n    onExportCsv: exportCsv,\n    onExportJson: exportJson,\n    onCopyTsv: () => copyTsv(),\n    onCopyMarkdown: () => copyMarkdown(),\n    onDensityChange: (v: 'compact' | 'cozy' | 'comfortable') => (densityOverride = v),\n    toolbarExtra,\n    customFilters,\n  })\n\n</script>\n\n<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions: the table container is a focusable keyboard-navigation surface (React/Vue parity: no role). -->\n<div\n  data-uipkge=\"\"\n  data-slot=\"data-table\"\n  class=\"focus-visible:ring-border/50 relative w-full outline-none focus-visible:ring-1\"\n  tabindex={0}\n  onkeydown={onTableKeyDown}\n>\n  <!-- Toolbar (above-mode: floats outside the bordered card). -->\n  {#if !hideToolbar && toolbarPosition === 'above'}\n    <DataTableToolbar {...toolbarProps} borderless />\n  {/if}\n\n  <div class={['bg-card text-card-foreground overflow-hidden', borderless === 'full' ? '' : 'rounded-md border'].join(' ')}>\n    <!-- Toolbar (inside-mode, default). -->\n    {#if !hideToolbar && toolbarPosition === 'inside'}\n      <DataTableToolbar {...toolbarProps} borderless={dropInnerBorders} />\n    {/if}\n\n    <!-- Filter Sheet (modal mode) -->\n    <DataTableFilterSheet\n      open={isFilterSheetOpen}\n      onOpenChange={onFilterSheetClose}\n      table={table as TanstackTable<any>}\n      {filters}\n      {activeFilterCount}\n      {isAnyFilterActive}\n      {isServerSide}\n      borderless={dropInnerBorders}\n      {getMultiSelectValue}\n      {getDateRangeValue}\n      {formatDateRange}\n      {getCalendarModel}\n      onApply={onApplyFilters}\n      onClearAll={clearAllFilters}\n      onToggleMultiselect={toggleMultiSelectValue}\n      onClearFilter={clearFilter}\n      onClearDateFilter={clearDateFilter}\n      {onCalendarUpdate}\n      onTextFilterUpdate={(col, val) => table.getColumn(col)?.setFilterValue(val)}\n      {customFilters}\n    />\n\n    <!-- Bulk action bar (inline mode) -->\n    {#if bulkActionPosition === 'inline' && renderBulkActions && selectedRows.length > 0}\n      <div class=\"bg-primary/5 border-primary/15 flex items-center gap-3 border-b px-4 py-2\">\n        <span class=\"bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-semibold tabular-nums\">\n          {selectedRows.length} selected\n        </span>\n        <div class=\"flex min-w-0 flex-1 items-center gap-2\">\n          {@render renderBulkActions(selectedRows, () => table.toggleAllRowsSelected(false))}\n        </div>\n        <button\n          type=\"button\"\n          class=\"text-muted-foreground hover:text-foreground shrink-0 text-xs font-medium transition\"\n          onclick={() => table.toggleAllRowsSelected(false)}\n        >\n          Clear\n        </button>\n      </div>\n    {/if}\n\n    <!-- Table. The Table primitive wraps the <table> in its own overflow-auto\n         div; neutralize that inner overflow and let the outer scroll\n         container own the scroll region. -->\n    <div\n      class={[densityClass, 'relative [&_[data-slot=table-container]]:overflow-visible', maxHeight ? 'overflow-auto' : ''].join(\n        ' ',\n      )}\n      style:max-height={maxHeight || undefined}\n    >\n      <Table>\n        <TableHeader\n          class={stickyHeader\n            ? 'bg-card/95 sticky top-0 z-10 shadow-[0_1px_0_0_var(--border)] backdrop-blur-sm'\n            : undefined}\n        >\n          {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}\n            <TableRow>\n              {#each headerGroup.headers as header (header.id)}\n                {@const reorderable =\n                  enableReorder &&\n                  header.column.id !== 'select' &&\n                  header.column.id !== 'actions' &&\n                  header.column.id !== 'expander'}\n                {@const sortDir = header.column.getIsSorted()}\n                <TableHead\n                  scope=\"col\"\n                  aria-sort={header.column.getCanSort()\n                    ? sortDir === 'asc'\n                      ? 'ascending'\n                      : sortDir === 'desc'\n                        ? 'descending'\n                        : 'none'\n                    : undefined}\n                  class={[\n                    'relative transition-colors duration-150',\n                    reorderable ? 'cursor-grab active:cursor-grabbing' : '',\n                    dragColId === header.column.id ? 'opacity-50' : '',\n                    dragOverColId === header.column.id && dragColId !== header.column.id\n                      ? 'border-foreground/60 border-l-2'\n                      : '',\n                  ].join(' ')}\n                  style={headStyle(header)}\n                  draggable={reorderable}\n                  ondragstart={reorderable ? (e) => onColDragStart(header.column.id, e) : undefined}\n                  ondragover={reorderable ? (e) => onColDragOver(header.column.id, e) : undefined}\n                  ondragend={reorderable ? () => onColDragEnd() : undefined}\n                  ondrop={reorderable ? () => onColDrop(header.column.id) : undefined}\n                >\n                  {#if !header.isPlaceholder}\n                    <FlexRender content={header.column.columnDef.header} context={header.getContext()} />\n                  {/if}\n                  {#if enableResize && header.column.getCanResize()}\n                    <!-- svelte-ignore a11y_no_static_element_interactions: pointer-only resize handle (React parity). -->\n                    <div\n                      class={[\n                        'hover:bg-foreground/30 absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none transition-colors select-none',\n                        header.column.getIsResizing() ? 'bg-foreground/60' : '',\n                      ].join(' ')}\n                      onmousedown={header.getResizeHandler()}\n                      ontouchstart={header.getResizeHandler()}\n                    ></div>\n                  {/if}\n                </TableHead>\n              {/each}\n            </TableRow>\n          {/each}\n        </TableHeader>\n        <TableBody\n          class={loading && bodyRows.length ? 'pointer-events-none opacity-60' : undefined}\n          aria-busy={loading || undefined}\n        >\n          {#if loading && !bodyRows.length}\n            {#each Array.from({ length: 5 }) as _, i (`sk-${i}`)}\n              <TableRow class=\"hover:bg-transparent\">\n                {#each Array.from({ length: Math.max(columns.length, 1) }) as __, j (`sk-${i}-${j}`)}\n                  <TableCell>\n                    <div\n                      class=\"bg-muted h-4 max-w-48 animate-pulse rounded\"\n                      style:width={`${48 + ((i * 17 + j * 23) % 40)}%`}\n                    ></div>\n                  </TableCell>\n                {/each}\n              </TableRow>\n            {/each}\n          {:else if bodyRows.length}\n            {#each bodyRows as row, idx (row.id)}\n              {#if row.getIsGrouped()}\n                <TableRow class=\"bg-muted/50 hover:bg-muted/70 cursor-pointer\" onclick={() => row.toggleExpanded()}>\n                  <TableCell colspan={row.getVisibleCells().length}>\n                    <button type=\"button\" class=\"flex items-center gap-2 text-sm font-medium\">\n                      {#if row.getIsExpanded()}\n                        <ChevronDown class=\"size-4\" />\n                      {:else}\n                        <ChevronRight class=\"size-4\" />\n                      {/if}\n                      <span>{String(row.groupingValue ?? '')}</span>\n                      <span class=\"text-muted-foreground font-normal\">{row.subRows.length}</span>\n                    </button>\n                  </TableCell>\n                </TableRow>\n              {:else}\n                <TableRow\n                  data-state={row.getIsSelected() ? 'selected' : undefined}\n                  data-focused={focusedRowIndex === idx ? 'true' : undefined}\n                  class={[\n                    onRowClick ? 'cursor-pointer' : '',\n                    focusedRowIndex === idx ? 'ring-primary/60 bg-muted/40 ring-1 ring-inset' : '',\n                    virtual ? '[contain-intrinsic-size:auto_48px] [content-visibility:auto]' : '',\n                  ]\n                    .filter(Boolean)\n                    .join(' ') || undefined}\n                  onclick={(e) => onRowClicked(row, idx, e)}\n                >\n                  {#each row.getVisibleCells() as cell (cell.id)}\n                    <TableCell class={row.getIsSelected() ? 'bg-muted' : 'bg-card'} style={pinStyle(cell.column)}>\n                      <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />\n                    </TableCell>\n                  {/each}\n                </TableRow>\n                {#if row.getIsExpanded() && renderExpanded}\n                  <TableRow>\n                    <TableCell colspan={row.getVisibleCells().length} class=\"bg-muted/30 px-6 py-4\">\n                      {@render renderExpanded(row.original, row)}\n                    </TableCell>\n                  </TableRow>\n                {/if}\n              {/if}\n            {/each}\n          {:else}\n            <TableRow class=\"hover:bg-transparent\">\n              <TableCell colspan={columns.length} class=\"h-36 text-center\">\n                {#if emptyState}\n                  {@render emptyState()}\n                {:else}\n                  <div class=\"text-muted-foreground flex flex-col items-center justify-center gap-2 py-8\">\n                    <div class=\"bg-muted/80 text-muted-foreground grid size-10 place-items-center rounded-full\">\n                      <Search class=\"size-4\" aria-hidden=\"true\" />\n                    </div>\n                    <div class=\"space-y-1\">\n                      <p class=\"text-foreground text-sm font-medium\">No results</p>\n                      <p class=\"text-muted-foreground max-w-[28ch] text-xs leading-relaxed\">\n                        Try adjusting search or filters to find what you need.\n                      </p>\n                    </div>\n                  </div>\n                {/if}\n              </TableCell>\n            </TableRow>\n          {/if}\n        </TableBody>\n        {#if renderFooter}\n          <tfoot class=\"bg-muted/20 sticky bottom-0 border-t\">{@render renderFooter(bodyRows)}</tfoot>\n        {/if}\n      </Table>\n      <!-- Infinite scroll sentinel -->\n      {#if infinite}\n        <div bind:this={sentinelEl} class=\"h-1\"></div>\n      {/if}\n      {#if infinite && loading}\n        <div class=\"border-border text-muted-foreground border-t px-4 py-3 text-center text-sm\">Loading more…</div>\n      {/if}\n    </div>\n\n    <!-- Pagination (inside-mode, default) -->\n    {#if enablePagination && !infinite && paginationPosition === 'inside'}\n      <DataTablePagination\n        table={table as TanstackTable<any>}\n        {totalRows}\n        {isServerSide}\n        borderless={dropInnerBorders}\n      />\n    {/if}\n  </div>\n\n  <!-- Pagination (below-mode: floats outside the bordered card). -->\n  {#if enablePagination && !infinite && paginationPosition === 'below'}\n    <DataTablePagination table={table as TanstackTable<any>} {totalRows} {isServerSide} borderless />\n  {/if}\n\n  <!-- Floating Bulk Actions HUD Dock — Linear/Raycast craft dock -->\n  {#if bulkActionPosition === 'floating' && selectedRows.length > 0}\n    <div\n      data-slot=\"data-table-bulk-dock\"\n      class=\"border-border/80 bg-background/95 text-foreground fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2.5 rounded-full border px-3.5 py-1.5 shadow-xl backdrop-blur-md select-none sm:absolute\"\n    >\n      <span\n        class=\"bg-primary/10 text-primary flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-xs font-semibold tabular-nums\"\n      >\n        {selectedRows.length} selected\n      </span>\n\n      <div class=\"bg-border h-3.5 w-px\"></div>\n\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-muted flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\"\n        title=\"Copy selected rows as TSV (Excel / Sheets)\"\n        onclick={() => copyTsv(selectedRows)}\n      >\n        <Copy class=\"size-3\" />\n        Copy TSV\n      </button>\n\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-muted flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors\"\n        title=\"Export table as CSV\"\n        onclick={exportCsv}\n      >\n        <Download class=\"size-3\" />\n        Export\n      </button>\n\n      {@render renderBulkActions?.(selectedRows, () => table.toggleAllRowsSelected(false))}\n\n      <div class=\"bg-border h-3.5 w-px\"></div>\n\n      <button\n        type=\"button\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-muted flex size-5 items-center justify-center rounded-full transition-colors\"\n        title=\"Deselect all (Esc)\"\n        aria-label=\"Clear selection\"\n        onclick={() => table.toggleAllRowsSelected(false)}\n      >\n        <X class=\"size-3.5\" />\n      </button>\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTable.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/DataTableColumnHeader.svelte",
      "content": "<!--\n  Sortable / hideable header cell for TanStack DataTable columns. Use it from\n  a column definition like:\n    header: ({ column }) => renderComponent(DataTableColumnHeader, { column, label: 'Email' })\n\n  Click on the label cycles sort asc → desc → none.\n\n  Optional per-column filter:\n    header: ({ column }) =>\n      renderComponent(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-->\n<script lang=\"ts\" module>\n  import type { Column } from '@tanstack/table-core'\n\n  interface FilterOption {\n    value: string\n    label: string\n  }\n  interface FilterDefinition {\n    column: string\n    label: string\n    type: 'text' | 'select' | 'multiselect' | 'date'\n    options?: (string | FilterOption)[]\n  }\n\n  export interface DataTableColumnHeaderProps<TData, TValue> {\n    column: Column<TData, TValue>\n    label: string\n    align?: 'left' | 'right' | 'center'\n    filter?: FilterDefinition\n    class?: string\n  }\n\n  function resolveOption(opt: string | FilterOption): FilterOption {\n    return typeof opt === 'string' ? { value: opt, label: opt } : opt\n  }\n</script>\n\n<script lang=\"ts\" generics=\"TData, TValue\">\n  import { untrack } from 'svelte'\n  import { ArrowUp, ArrowUpDown, Check, Filter, FilterX } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { Button } from '$lib/components/ui/button'\n  import { Input } from '$lib/components/ui/input'\n  import { Popover, PopoverContent, PopoverTrigger } from '$lib/components/ui/popover'\n  import type { DateRange } from '$lib/components/ui/range-calendar'\n  import {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n    CommandList,\n  } from '$lib/components/ui/command'\n  import DataTableRangeCalendar from './DataTableRangeCalendar.svelte'\n  import { dateToIso, isoRangeToCalendar } from './date-utils'\n\n  let { column, label, align = 'left', filter, class: className }: DataTableColumnHeaderProps<TData, TValue> = $props()\n\n  let open = $state(false)\n\n  function next() {\n    const current = column.getIsSorted()\n    if (!current) column.toggleSorting(false)\n    else if (current === 'asc') column.toggleSorting(true)\n    else column.clearSorting()\n  }\n\n  const filterValue = $derived(column.getFilterValue())\n\n  const isFilterActive = $derived.by(() => {\n    const v = filterValue\n    if (v === undefined || v === null || v === '') return false\n    if (Array.isArray(v)) return v.length > 0\n    if (typeof v === 'object') return Object.keys(v).length > 0\n    return true\n  })\n\n  // Text-input bound separately so we can apply on blur / Enter rather than\n  // thrashing the column filter on every keystroke.\n  let textDraft = $state('')\n  $effect(() => {\n    if (open && filter?.type === 'text') {\n      textDraft = untrack(() => (filterValue as string) ?? '')\n    }\n  })\n\n  function applyText() {\n    column.setFilterValue(textDraft || undefined)\n  }\n\n  function clearText() {\n    textDraft = ''\n    column.setFilterValue(undefined)\n  }\n\n  function toggleMultiselect(value: string) {\n    const current = (filterValue as string[]) ?? []\n    const nextVal = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]\n    column.setFilterValue(nextVal.length > 0 ? nextVal : undefined)\n  }\n\n  function selectOne(value: string) {\n    column.setFilterValue(value || undefined)\n    open = false\n  }\n\n  function clearFilter() {\n    column.setFilterValue(undefined)\n    textDraft = ''\n  }\n\n  // Date-range bridging. The column stores ISO strings; the calendar wants\n  // native Date instances. Reads/writes both shapes.\n  const dateModel = $derived(isoRangeToCalendar(filterValue as { from?: string; to?: string } | undefined))\n\n  function onDateSelect(range: DateRange | undefined) {\n    const isoFrom = range?.start ? dateToIso(range.start) : undefined\n    const isoTo = range?.end ? dateToIso(range.end) : undefined\n    if (!isoFrom && !isoTo) {\n      column.setFilterValue(undefined)\n    } else {\n      column.setFilterValue({ from: isoFrom, to: isoTo })\n    }\n  }\n\n  function selectedLabels(): string[] {\n    if (!filter?.options) return []\n    const selected = (filterValue as string[]) ?? []\n    return filter.options\n      .map(resolveOption)\n      .filter((o) => selected.includes(o.value))\n      .map((o) => o.label)\n  }\n</script>\n\n<div\n  class={cn(\n    'flex items-center gap-0.5',\n    align === 'right' && 'justify-end',\n    align === 'center' && 'justify-center',\n    className,\n  )}\n>\n  {#if column.getCanSort()}\n    <button\n      type=\"button\"\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      onclick={next}\n    >\n      <span>{label}</span>\n      {#if column.getIsSorted()}\n        <ArrowUp\n          class={cn(\n            'text-foreground size-3.5 transition-transform duration-200 ease-in-out',\n            column.getIsSorted() === 'desc' ? 'rotate-180' : 'rotate-0',\n          )}\n        />\n      {:else}\n        <ArrowUpDown class=\"size-3.5 opacity-40 transition-opacity duration-150 group-hover:opacity-70\" />\n      {/if}\n    </button>\n  {:else}\n    <span class=\"text-muted-foreground text-sm font-medium\">{label}</span>\n  {/if}\n\n  <!-- Optional per-column header filter -->\n  {#if filter}\n    <Popover bind:open>\n      <PopoverTrigger>\n        {#snippet child({ props })}\n          <button\n            {...props}\n            type=\"button\"\n            class={cn(\n              'text-muted-foreground relative inline-flex size-6 items-center justify-center rounded transition-colors',\n              'hover:text-foreground hover:bg-muted/60',\n              isFilterActive && 'text-foreground',\n            )}\n            aria-label={`Filter ${label}`}\n          >\n            <Filter class=\"size-3.5\" />\n            {#if isFilterActive}\n              <span aria-hidden=\"true\" class=\"bg-primary absolute -top-0.5 -right-0.5 size-1.5 rounded-full\"></span>\n            {/if}\n          </button>\n        {/snippet}\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          {#if isFilterActive}\n            <button\n              type=\"button\"\n              class=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1 transition-colors\"\n              onclick={clearFilter}\n            >\n              <FilterX class=\"size-3\" />\n              Clear\n            </button>\n          {/if}\n        </div>\n\n        <!-- TEXT -->\n        {#if filter.type === 'text'}\n          <div class=\"space-y-2 p-3\">\n            <Input\n              value={textDraft}\n              placeholder={`Search ${filter.label.toLowerCase()}…`}\n              class=\"h-8\"\n              onValueChange={(v) => (textDraft = v)}\n              onkeydown={(e) => {\n                if (e.key === 'Enter') {\n                  applyText()\n                  open = false\n                }\n              }}\n              onfocusout={applyText}\n            />\n            <div class=\"flex gap-2\">\n              <Button\n                size=\"sm\"\n                class=\"h-8 flex-1\"\n                onclick={() => {\n                  applyText()\n                  open = false\n                }}\n              >\n                Apply\n              </Button>\n              <Button size=\"sm\" variant=\"outline\" class=\"h-8\" onclick={clearText}>Clear</Button>\n            </div>\n          </div>\n        {/if}\n\n        <!-- SELECT / MULTISELECT -->\n        {#if filter.type === 'select' || filter.type === 'multiselect'}\n          <Command class=\"max-h-[280px]\">\n            <CommandInput placeholder={`Search ${filter.label.toLowerCase()}…`} class=\"h-8\" />\n            <CommandList>\n              <CommandEmpty>No matches.</CommandEmpty>\n              <CommandGroup>\n                {#each filter.options ?? [] as opt (resolveOption(opt).value)}\n                  {@const o = resolveOption(opt)}\n                  <CommandItem\n                    value={o.value}\n                    onSelect={() => (filter.type === 'multiselect' ? toggleMultiselect(o.value) : selectOne(o.value))}\n                  >\n                    {#if filter.type === 'multiselect'}\n                      <div\n                        class={cn(\n                          'border-primary/50 mr-2 flex size-4 items-center justify-center rounded-sm border transition-colors',\n                          ((filterValue as string[]) ?? []).includes(o.value)\n                            ? 'bg-primary text-primary-foreground'\n                            : 'opacity-50',\n                        )}\n                      >\n                        <Check class=\"size-3\" />\n                      </div>\n                    {:else}\n                      <Check class={cn('mr-2 size-4', filterValue === o.value ? 'opacity-100' : 'opacity-0')} />\n                    {/if}\n                    <span>{o.label}</span>\n                  </CommandItem>\n                {/each}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        {/if}\n\n        <!-- DATE RANGE -->\n        {#if filter.type === 'date'}\n          <div class=\"p-2\">\n            <DataTableRangeCalendar value={dateModel} onValueChange={onDateSelect} />\n            <div class=\"flex gap-2 px-1 pt-2\">\n              <Button size=\"sm\" class=\"h-8 flex-1\" onclick={() => (open = false)}>Apply</Button>\n              <Button size=\"sm\" variant=\"outline\" class=\"h-8\" onclick={clearFilter}>Clear</Button>\n            </div>\n          </div>\n        {/if}\n\n        <!-- Active selection summary -->\n        {#if isFilterActive && (filter.type === 'multiselect' || filter.type === 'select')}\n          <div class=\"border-border text-muted-foreground border-t px-3 py-2 text-xs\">\n            {#if filter.type === 'multiselect'}\n              <span>\n                {selectedLabels().length} selected:\n                <span class=\"text-foreground\">{selectedLabels().join(', ')}</span>\n              </span>\n            {:else}\n              <span>\n                <span class=\"text-foreground\">{filterValue as string}</span>\n              </span>\n            {/if}\n          </div>\n        {/if}\n      </PopoverContent>\n    </Popover>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTableColumnHeader.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/DataTableFilterPopover.svelte",
      "content": "<!--\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 call `onCommitDraft`\n  with the full draft and the parent walks the entries calling\n  `setFilterValue` per column. Closing the popover (Escape / click out)\n  simply discards the draft.\n-->\n<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { Table } from '@tanstack/table-core'\n  import type { DateRange } from '$lib/components/ui/range-calendar'\n  import type { FilterDefinition } from './types'\n\n  type DraftDateValue = { from?: string; to?: string }\n  type DraftValue = string[] | string | DraftDateValue | undefined\n  type Draft = Record<string, DraftValue>\n\n  export interface DataTableFilterPopoverProps {\n    table: Table<any>\n    filters: FilterDefinition[]\n    activeFilterCount: number\n    isAnyFilterActive: boolean\n    isServerSide: boolean\n    getMultiSelectValue: (column: string) => string[]\n    getDateRangeValue: (column: string) => { from?: string; to?: string }\n    formatDateRange: (column: string) => string\n    getCalendarModel: (column: string) => DateRange | undefined\n    // Committed draft on Apply -- a `Record<columnId, value>` where each\n    // value is the final shape TanStack's `setFilterValue` expects.\n    onCommitDraft: (draft: Draft) => void\n    onClearAll: () => void\n    customFilters?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Check, SlidersHorizontal, X } from '@lucide/svelte'\n  import { Input } from '$lib/components/ui/input'\n  import { Badge } from '$lib/components/ui/badge'\n  import { Button } from '$lib/components/ui/button'\n  import { Label } from '$lib/components/ui/label'\n  import { Popover, PopoverContent, PopoverTrigger } from '$lib/components/ui/popover'\n  import {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n    CommandList,\n  } from '$lib/components/ui/command'\n  import DataTableRangeCalendar from './DataTableRangeCalendar.svelte'\n  import { resolveOption } from './types'\n  import { dateToIso, isoRangeToCalendar } from './date-utils'\n\n  let {\n    table,\n    filters,\n    activeFilterCount,\n    isServerSide,\n    getMultiSelectValue,\n    getDateRangeValue,\n    onCommitDraft,\n    customFilters,\n  }: DataTableFilterPopoverProps = $props()\n\n  let open = $state(false)\n  let draft = $state<Draft>({})\n  let filterScrollEl = $state<HTMLDivElement | null>(null)\n\n  function seedDraft(): Draft {\n    const next: Draft = {}\n    for (const f of filters) {\n      if (f.type === 'multiselect' || f.type === 'select') {\n        next[f.column] = [...getMultiSelectValue(f.column)]\n      } else if (f.type === 'date') {\n        const dr = getDateRangeValue(f.column)\n        next[f.column] = { from: dr.from, to: dr.to }\n      } else if (f.type === 'text') {\n        const v = table.getColumn(f.column)?.getFilterValue() as string | undefined\n        next[f.column] = v ?? ''\n      }\n    }\n    return next\n  }\n\n  function getDraftMulti(column: string): string[] {\n    const v = draft[column]\n    return Array.isArray(v) ? v : []\n  }\n\n  function getDraftText(column: string): string {\n    const v = draft[column]\n    return typeof v === 'string' ? v : ''\n  }\n\n  function getDraftDate(column: string): DraftDateValue {\n    const v = draft[column]\n    if (v && typeof v === 'object' && !Array.isArray(v)) return v as DraftDateValue\n    return {}\n  }\n\n  function toggleDraftMulti(column: string, option: string) {\n    const current = getDraftMulti(column)\n    const next = current.includes(option) ? current.filter((v) => v !== option) : [...current, option]\n    draft = { ...draft, [column]: next }\n  }\n\n  function setDraftText(column: string, value: string) {\n    draft = { ...draft, [column]: value }\n  }\n\n  function clearDraftSection(filter: FilterDefinition) {\n    if (filter.type === 'multiselect' || filter.type === 'select') {\n      draft = { ...draft, [filter.column]: [] }\n    } else if (filter.type === 'date') {\n      draft = { ...draft, [filter.column]: {} }\n    } else if (filter.type === 'text') {\n      draft = { ...draft, [filter.column]: '' }\n    }\n  }\n\n  function resetDraft() {\n    const next: Draft = {}\n    for (const f of filters) {\n      if (f.type === 'multiselect' || f.type === 'select') next[f.column] = []\n      else if (f.type === 'date') next[f.column] = {}\n      else if (f.type === 'text') next[f.column] = ''\n    }\n    draft = next\n  }\n\n  // ── Date helpers (local; popover is fully self-contained for draft) ──\n  function getDraftCalendarModel(column: string): DateRange | undefined {\n    return isoRangeToCalendar(getDraftDate(column))\n  }\n\n  function onDraftCalendarUpdate(column: string, val: DateRange | undefined) {\n    const from = val?.start ? dateToIso(val.start) : undefined\n    const to = val?.end ? dateToIso(val.end) : undefined\n    draft = { ...draft, [column]: { from, to } }\n  }\n\n  function formatDraftDateRange(column: string): string {\n    const dr = getDraftDate(column)\n    if (dr.from && dr.to) return `${dr.from} - ${dr.to}`\n    if (dr.from) return `From ${dr.from}`\n    if (dr.to) return `Until ${dr.to}`\n    return ''\n  }\n\n  function isDraftSectionActive(filter: FilterDefinition): boolean {\n    if (filter.type === 'multiselect' || filter.type === 'select') {\n      return getDraftMulti(filter.column).length > 0\n    }\n    if (filter.type === 'date') {\n      const dr = getDraftDate(filter.column)\n      return !!(dr.from || dr.to)\n    }\n    if (filter.type === 'text') {\n      return !!getDraftText(filter.column)\n    }\n    return false\n  }\n\n  // `Reset` is disabled when the draft has nothing to clear.\n  const isDraftDirty = $derived(filters.some(isDraftSectionActive))\n\n  function handleOpenChange(isOpen: boolean) {\n    if (isOpen) {\n      draft = seedDraft()\n      open = true\n      setTimeout(() => filterScrollEl?.scrollTo({ top: 0 }), 50)\n    } else {\n      // Close-without-apply: nothing to commit. Real column filters were never\n      // mutated by the popover; the draft is local and discarded.\n      draft = {}\n      open = false\n    }\n  }\n\n  function applyAndClose() {\n    const snapshot: Draft = {}\n    for (const f of filters) {\n      const v = draft[f.column]\n      if (f.type === 'multiselect' || f.type === 'select') {\n        const arr = Array.isArray(v) ? v : []\n        snapshot[f.column] = arr.length > 0 ? arr : undefined\n      } else if (f.type === 'date') {\n        const dr = v && typeof v === 'object' && !Array.isArray(v) ? (v as DraftDateValue) : {}\n        snapshot[f.column] = dr.from || dr.to ? { from: dr.from, to: dr.to } : undefined\n      } else if (f.type === 'text') {\n        const s = typeof v === 'string' ? v : ''\n        snapshot[f.column] = s || undefined\n      }\n    }\n    onCommitDraft(snapshot)\n    open = false\n  }\n\n  const filteredRowCount = $derived(table.getFilteredRowModel().rows.length)\n</script>\n\n<Popover {open} onOpenChange={handleOpenChange}>\n  <PopoverTrigger>\n    {#snippet child({ props })}\n      <Button\n        {...props}\n        variant=\"outline\"\n        size=\"sm\"\n        class={[\n          'h-8 gap-2',\n          activeFilterCount > 0 ? 'border-primary/40 bg-primary/5 text-primary hover:bg-primary/10' : '',\n        ].join(' ')}\n      >\n        <SlidersHorizontal class=\"size-4\" />\n        Filters\n        {#if activeFilterCount > 0}\n          <Badge class=\"bg-primary text-primary-foreground ml-0.5 size-5 rounded-full p-0 text-xs font-semibold\">\n            {activeFilterCount}\n          </Badge>\n        {/if}\n      </Button>\n    {/snippet}\n  </PopoverTrigger>\n  <PopoverContent align=\"start\" class=\"flex max-h-[min(560px,80vh)] w-[380px] flex-col overflow-hidden p-0\">\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\">Filters</p>\n          <p class=\"text-muted-foreground mt-1 text-xs\">\n            {#if activeFilterCount > 0}\n              {activeFilterCount} active{#if !isServerSide}{' '}&middot; {filteredRowCount} result{filteredRowCount !== 1\n                  ? 's'\n                  : ''}{/if}\n            {:else}\n              Narrow down results\n            {/if}\n          </p>\n        </div>\n      </div>\n    </div>\n\n    <!-- Scrollable filter sections -->\n    <div bind:this={filterScrollEl} class=\"flex-1 overflow-y-auto\">\n      <div class=\"space-y-2 p-3\">\n        {#each filters as filter (filter.column)}\n          {#if filter.type === 'text'}\n            <div class=\"bg-muted/40 rounded-lg p-2.5\">\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                {#if getDraftText(filter.column)}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                    onclick={() => clearDraftSection(filter)}\n                  >\n                    Clear\n                  </button>\n                {/if}\n              </div>\n              <Input\n                placeholder={`Filter by ${filter.label.toLowerCase()}...`}\n                value={getDraftText(filter.column)}\n                class=\"h-8 text-sm\"\n                onValueChange={(v) => setDraftText(filter.column, v ?? '')}\n              />\n            </div>\n          {:else if filter.type === 'multiselect' || filter.type === 'select'}\n            {@const multi = getDraftMulti(filter.column)}\n            <div\n              class={[\n                'rounded-lg p-2.5 transition-colors',\n                multi.length > 0 ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n              ].join(' ')}\n            >\n              <div 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                  {#if multi.length > 0}\n                    <Badge\n                      variant=\"secondary\"\n                      class=\"bg-primary/15 text-primary h-4 rounded-full px-1.5 text-xs font-semibold\"\n                    >\n                      {multi.length}\n                    </Badge>\n                  {/if}\n                </div>\n                {#if multi.length > 0}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                    onclick={() => clearDraftSection(filter)}\n                  >\n                    Clear\n                  </button>\n                {/if}\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 class=\"h-7 text-sm\" placeholder={`Search ${filter.label.toLowerCase()}...`} />\n                <CommandList class=\"mt-1 max-h-[132px]\">\n                  <CommandEmpty>No results.</CommandEmpty>\n                  <CommandGroup class=\"p-0\">\n                    {#each filter.options ?? [] as rawOpt (resolveOption(rawOpt).value)}\n                      {@const opt = resolveOption(rawOpt)}\n                      {@const OptIcon = opt.icon}\n                      <CommandItem\n                        value={opt.label}\n                        class=\"rounded-md px-2 py-1.5 text-sm\"\n                        onSelect={() => toggleDraftMulti(filter.column, opt.value)}\n                      >\n                        <div\n                          class={[\n                            'flex size-4 shrink-0 items-center justify-center rounded-sm border transition-colors',\n                            multi.includes(opt.value)\n                              ? 'border-primary bg-primary text-primary-foreground'\n                              : 'border-muted-foreground/40 [&_svg]:invisible',\n                          ].join(' ')}\n                        >\n                          <Check class=\"size-3\" />\n                        </div>\n                        {#if OptIcon}<OptIcon class=\"text-muted-foreground size-4\" />{/if}\n                        <span>{opt.label}</span>\n                      </CommandItem>\n                    {/each}\n                  </CommandGroup>\n                </CommandList>\n              </Command>\n            </div>\n          {:else if filter.type === 'date'}\n            {@const dr = getDraftDate(filter.column)}\n            {@const hasDate = !!(dr.from || dr.to)}\n            <div\n              class={[\n                'rounded-lg p-2.5 transition-colors',\n                hasDate ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n              ].join(' ')}\n            >\n              <div 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                  {#if hasDate}\n                    <Badge\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                  {/if}\n                </div>\n                {#if hasDate}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                    onclick={() => clearDraftSection(filter)}\n                  >\n                    Clear\n                  </button>\n                {/if}\n              </div>\n              <div class=\"flex justify-center overflow-hidden rounded-md border\">\n                <DataTableRangeCalendar\n                  value={getDraftCalendarModel(filter.column)}\n                  numberOfMonths={1}\n                  class=\"p-2\"\n                  onValueChange={(range) => onDraftCalendarUpdate(filter.column, range)}\n                />\n              </div>\n            </div>\n          {/if}\n        {/each}\n\n        <!-- Consumer-supplied custom filter UI -->\n        {@render customFilters?.()}\n      </div>\n    </div>\n\n    <!-- Footer -->\n    <div class=\"flex gap-2 border-t px-3 py-2.5\">\n      <Button variant=\"outline\" size=\"sm\" class=\"h-8 flex-1\" disabled={!isDraftDirty} onclick={resetDraft}>\n        <X class=\"size-3.5\" />\n        Reset\n      </Button>\n      <Button size=\"sm\" class=\"h-8 flex-1\" onclick={applyAndClose}>Apply</Button>\n    </div>\n  </PopoverContent>\n</Popover>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTableFilterPopover.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/DataTableFilterSheet.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { Table } from '@tanstack/table-core'\n  import type { DateRange } from '$lib/components/ui/range-calendar'\n  import type { FilterDefinition } from './types'\n\n  export interface DataTableFilterSheetProps {\n    open: boolean\n    onOpenChange: (open: boolean) => void\n    table: Table<any>\n    filters: FilterDefinition[]\n    activeFilterCount: number\n    isAnyFilterActive: boolean\n    isServerSide: boolean\n    /** Strip section bgs / rings / dividers / SheetContent side border. */\n    borderless?: boolean\n    getMultiSelectValue: (column: string) => string[]\n    getDateRangeValue: (column: string) => { from?: string; to?: string }\n    formatDateRange: (column: string) => string\n    getCalendarModel: (column: string) => DateRange | undefined\n    onApply: () => void\n    onClearAll: () => void\n    onToggleMultiselect: (column: string, value: string) => void\n    onClearFilter: (filter: FilterDefinition) => void\n    onClearDateFilter: (filter: FilterDefinition) => void\n    onCalendarUpdate: (column: string, value: DateRange | undefined) => void\n    onTextFilterUpdate: (column: string, value: string | undefined) => void\n    customFilters?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Check, SlidersHorizontal, X } from '@lucide/svelte'\n  import { Input } from '$lib/components/ui/input'\n  import { Badge } from '$lib/components/ui/badge'\n  import { Button } from '$lib/components/ui/button'\n  import { Label } from '$lib/components/ui/label'\n  import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '$lib/components/ui/sheet'\n  import {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n    CommandList,\n  } from '$lib/components/ui/command'\n  import DataTableRangeCalendar from './DataTableRangeCalendar.svelte'\n  import { resolveOption } from './types'\n\n  let {\n    open,\n    onOpenChange,\n    table,\n    filters,\n    activeFilterCount,\n    isAnyFilterActive,\n    isServerSide,\n    borderless = false,\n    getMultiSelectValue,\n    getDateRangeValue,\n    formatDateRange,\n    getCalendarModel,\n    onApply,\n    onClearAll,\n    onToggleMultiselect,\n    onClearFilter,\n    onClearDateFilter,\n    onCalendarUpdate,\n    onTextFilterUpdate,\n    customFilters,\n  }: DataTableFilterSheetProps = $props()\n\n  let filterScrollEl = $state<HTMLDivElement | null>(null)\n\n  $effect(() => {\n    if (open) {\n      const t = setTimeout(() => {\n        filterScrollEl?.scrollTo({ top: 0 })\n      }, 50)\n      return () => clearTimeout(t)\n    }\n  })\n\n  const filteredRowCount = $derived(table.getFilteredRowModel().rows.length)\n</script>\n\n<Sheet {open} {onOpenChange}>\n  <SheetContent class={['flex flex-col gap-0 p-0 sm:max-w-[400px]', borderless ? 'border-0' : ''].join(' ')}>\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\">Filters</SheetTitle>\n            <SheetDescription class=\"text-xs\">\n              {#if activeFilterCount > 0}\n                {activeFilterCount} active filter{activeFilterCount > 1 ? 's' : ''}{#if !isServerSide}{' '}&middot;\n                  {filteredRowCount} result{filteredRowCount !== 1 ? 's' : ''}{/if}\n              {:else}\n                Narrow down results\n              {/if}\n            </SheetDescription>\n          </SheetHeader>\n        </div>\n      </div>\n    </div>\n\n    <!-- Scrollable filter sections -->\n    <div bind:this={filterScrollEl} class=\"flex-1 overflow-y-auto\">\n      <div class=\"space-y-2 p-4\">\n        {#each filters as filter (filter.column)}\n          {@const textValue = (table.getColumn(filter.column)?.getFilterValue() as string) ?? ''}\n          {@const multi = getMultiSelectValue(filter.column)}\n          {@const dr = getDateRangeValue(filter.column)}\n          {@const hasDate = !!(dr.from || dr.to)}\n\n          {#if filter.type === 'text'}\n            <div class={borderless ? 'py-2' : 'bg-muted/40 rounded-lg p-3'}>\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                {#if textValue}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                    onclick={() => onTextFilterUpdate(filter.column, undefined)}\n                  >\n                    Clear\n                  </button>\n                {/if}\n              </div>\n              <Input\n                placeholder={`Filter by ${filter.label.toLowerCase()}...`}\n                value={textValue}\n                class=\"h-8 text-sm\"\n                onValueChange={(v) => onTextFilterUpdate(filter.column, v || undefined)}\n              />\n            </div>\n          {:else if filter.type === 'multiselect' || filter.type === 'select'}\n            <div\n              class={borderless\n                ? 'py-2 transition-colors'\n                : [\n                    'rounded-lg p-3 transition-colors',\n                    multi.length > 0 ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n                  ].join(' ')}\n            >\n              <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                  {#if multi.length > 0}\n                    <Badge\n                      variant=\"secondary\"\n                      class=\"bg-primary/15 text-primary h-4 rounded-full px-1.5 text-xs font-semibold\"\n                    >\n                      {multi.length}\n                    </Badge>\n                  {/if}\n                </div>\n                {#if multi.length > 0}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                    onclick={() => onClearFilter(filter)}\n                  >\n                    Clear\n                  </button>\n                {/if}\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 class=\"h-7 text-sm\" placeholder={`Search ${filter.label.toLowerCase()}...`} />\n                <CommandList class=\"mt-1 max-h-[132px]\">\n                  <CommandEmpty>No results.</CommandEmpty>\n                  <CommandGroup class=\"p-0\">\n                    {#each filter.options ?? [] as rawOpt (resolveOption(rawOpt).value)}\n                      {@const opt = resolveOption(rawOpt)}\n                      {@const OptIcon = opt.icon}\n                      <CommandItem\n                        value={opt.label}\n                        class=\"rounded-md px-2 py-1.5 text-sm\"\n                        onSelect={() => onToggleMultiselect(filter.column, opt.value)}\n                      >\n                        <div\n                          class={[\n                            'flex size-4 shrink-0 items-center justify-center rounded-sm border transition-colors',\n                            multi.includes(opt.value)\n                              ? 'border-primary bg-primary text-primary-foreground'\n                              : 'border-muted-foreground/40 [&_svg]:invisible',\n                          ].join(' ')}\n                        >\n                          <Check class=\"size-3\" />\n                        </div>\n                        {#if OptIcon}<OptIcon class=\"text-muted-foreground size-4\" />{/if}\n                        <span>{opt.label}</span>\n                      </CommandItem>\n                    {/each}\n                  </CommandGroup>\n                </CommandList>\n              </Command>\n            </div>\n          {:else if filter.type === 'date'}\n            <div\n              class={borderless\n                ? 'py-2 transition-colors'\n                : [\n                    'rounded-lg p-3 transition-colors',\n                    hasDate ? 'bg-primary/[0.04] ring-primary/20 ring-1' : 'bg-muted/40',\n                  ].join(' ')}\n            >\n              <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                  {#if hasDate}\n                    <Badge\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                  {/if}\n                </div>\n                {#if hasDate}\n                  <button\n                    type=\"button\"\n                    class=\"text-muted-foreground hover:text-foreground text-xs transition-colors\"\n                    onclick={() => onClearDateFilter(filter)}\n                  >\n                    Clear\n                  </button>\n                {/if}\n              </div>\n              <div class={['flex justify-center overflow-hidden', borderless ? '' : 'rounded-md border'].join(' ')}>\n                <DataTableRangeCalendar\n                  value={getCalendarModel(filter.column)}\n                  numberOfMonths={1}\n                  class=\"p-2\"\n                  onValueChange={(range) => onCalendarUpdate(filter.column, range)}\n                />\n              </div>\n            </div>\n          {/if}\n        {/each}\n\n        <!-- Consumer-supplied custom filter UI -->\n        {@render customFilters?.()}\n      </div>\n    </div>\n\n    <!-- Footer -->\n    <div class={['flex gap-2 px-4 py-3', borderless ? '' : 'border-t'].join(' ')}>\n      <Button variant=\"outline\" size=\"sm\" class=\"flex-1\" disabled={!isAnyFilterActive} onclick={onClearAll}>\n        <X class=\"size-3.5\" />\n        Reset All\n      </Button>\n      <Button size=\"sm\" class=\"flex-1\" onclick={onApply}>\n        {#if isServerSide}\n          Apply Filters\n        {:else}\n          Show {filteredRowCount} result{filteredRowCount !== 1 ? 's' : ''}\n        {/if}\n      </Button>\n    </div>\n  </SheetContent>\n</Sheet>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTableFilterSheet.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/DataTablePagination.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Table } from '@tanstack/table-core'\n\n  export interface DataTablePaginationProps {\n    table: Table<any>\n    totalRows: number\n    isServerSide: boolean\n    borderless?: boolean\n  }\n</script>\n\n<script lang=\"ts\">\n  import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'\n\n  let { table, totalRows, isServerSide, borderless = false }: DataTablePaginationProps = $props()\n\n  const selected = $derived(table.getFilteredSelectedRowModel().rows.length)\n  const total = $derived(isServerSide ? totalRows : table.getFilteredRowModel().rows.length)\n</script>\n\n<div\n  class={[\n    'flex flex-col gap-3 py-3 sm:flex-row sm:items-center sm:justify-between',\n    borderless ? '' : 'border-t px-4',\n  ].join(' ')}\n>\n  <div class=\"text-muted-foreground text-sm tabular-nums\">\n    <span class=\"text-foreground font-medium\">{selected}</span> of {total} row(s) selected\n  </div>\n  <div class=\"flex flex-wrap items-center gap-4 sm:gap-6\">\n    <div class=\"flex items-center gap-2\">\n      <p class=\"text-sm font-medium whitespace-nowrap\">Rows per page</p>\n      <Select\n        value={`${table.getState().pagination.pageSize}`}\n        onValueChange={(value) => {\n          table.setPageSize(Number(value))\n          table.setPageIndex(0)\n        }}\n      >\n        <SelectTrigger class=\"h-8 w-[4.5rem]\">\n          <SelectValue placeholder={`${table.getState().pagination.pageSize}`} />\n        </SelectTrigger>\n        <SelectContent side=\"top\">\n          {#each [10, 20, 30, 40, 50] as pageSize (pageSize)}\n            <SelectItem value={`${pageSize}`}>{pageSize}</SelectItem>\n          {/each}\n        </SelectContent>\n      </Select>\n    </div>\n    <div class=\"text-sm font-medium whitespace-nowrap tabular-nums\">\n      Page {table.getState().pagination.pageIndex + 1} of {Math.max(table.getPageCount(), 1)}\n    </div>\n    <div class=\"flex items-center gap-1\">\n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        class=\"hidden size-8 lg:flex\"\n        disabled={!table.getCanPreviousPage()}\n        onclick={() => table.setPageIndex(0)}\n      >\n        <ChevronsLeft class=\"size-4\" aria-hidden=\"true\" />\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        onclick={() => table.previousPage()}\n      >\n        <ChevronLeft class=\"size-4\" aria-hidden=\"true\" />\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        onclick={() => table.nextPage()}\n      >\n        <ChevronRight class=\"size-4\" aria-hidden=\"true\" />\n        <span class=\"sr-only\">Next page</span>\n      </Button>\n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        class=\"hidden size-8 lg:flex\"\n        disabled={!table.getCanNextPage()}\n        onclick={() => table.setPageIndex(table.getPageCount() - 1)}\n      >\n        <ChevronsRight class=\"size-4\" aria-hidden=\"true\" />\n        <span class=\"sr-only\">Last page</span>\n      </Button>\n    </div>\n  </div>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTablePagination.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/DataTableToolbar.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { Table } from '@tanstack/table-core'\n  import type { DateRange } from '$lib/components/ui/range-calendar'\n  import type { FilterDefinition } from './types'\n\n  export interface DataTableToolbarProps {\n    table: Table<any>\n    filterColumn: string\n    filterPlaceholder: string\n    filters: FilterDefinition[]\n    filterMode: 'inline' | 'modal' | 'popover'\n    enableSearch?: boolean\n    enableColumnVisibility?: boolean\n    enableExport?: boolean\n    enableDensityToggle?: boolean\n    density?: 'compact' | 'cozy' | 'comfortable'\n    borderless?: boolean\n    activeFilterCount: number\n    isAnyFilterActive: boolean\n    isServerSide: boolean\n    getMultiSelectValue: (column: string) => string[]\n    getDateRangeValue: (column: string) => { from?: string; to?: string }\n    getFilterSelectedLabels: (filter: FilterDefinition) => string[]\n    formatDateRange: (column: string) => string\n    getCalendarModel: (column: string) => DateRange | undefined\n    onSearch: (value: string) => void\n    onOpenFilterSheet: () => void\n    onApplyFilters: () => void\n    onClearAllFilters: () => void\n    onToggleMultiselect: (column: string, value: string) => void\n    onClearFilter: (filter: FilterDefinition) => void\n    onClearDateFilter: (filter: FilterDefinition) => void\n    onCalendarUpdate: (column: string, value: DateRange | undefined) => void\n    onTextFilterUpdate: (column: string, value: string | undefined) => void\n    onCommitFilters: (draft: Record<string, any>) => void\n    onExportCsv: () => void\n    onExportJson: () => void\n    onCopyTsv?: () => void\n    onCopyMarkdown?: () => void\n    onDensityChange: (value: 'compact' | 'cozy' | 'comfortable') => void\n    toolbarExtra?: Snippet\n    customFilters?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import {\n    CalendarIcon,\n    Check,\n    ChevronDown,\n    Columns3,\n    Download,\n    Plus,\n    Rows3,\n    Search,\n    SlidersHorizontal,\n    X,\n  } from '@lucide/svelte'\n  import { Input } from '$lib/components/ui/input'\n  import { Badge } from '$lib/components/ui/badge'\n  import { Button } from '$lib/components/ui/button'\n  import { Separator } from '$lib/components/ui/separator'\n  import {\n    DropdownMenu,\n    DropdownMenuCheckboxItem,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuRadioGroup,\n    DropdownMenuRadioItem,\n    DropdownMenuTrigger,\n  } from '$lib/components/ui/dropdown-menu'\n  import {\n    Command,\n    CommandEmpty,\n    CommandGroup,\n    CommandInput,\n    CommandItem,\n    CommandList,\n    CommandSeparator,\n  } from '$lib/components/ui/command'\n  import { Popover, PopoverContent, PopoverTrigger } from '$lib/components/ui/popover'\n  import DataTableRangeCalendar from './DataTableRangeCalendar.svelte'\n  import DataTableFilterPopover from './DataTableFilterPopover.svelte'\n  import { resolveOption } from './types'\n\n  let {\n    table,\n    filterColumn,\n    filterPlaceholder,\n    filters,\n    filterMode,\n    enableSearch = true,\n    enableColumnVisibility = true,\n    enableExport = false,\n    enableDensityToggle = false,\n    density = 'cozy',\n    borderless = false,\n    activeFilterCount,\n    isAnyFilterActive,\n    isServerSide,\n    getMultiSelectValue,\n    getDateRangeValue,\n    getFilterSelectedLabels,\n    formatDateRange,\n    getCalendarModel,\n    onSearch,\n    onOpenFilterSheet,\n    onClearAllFilters,\n    onToggleMultiselect,\n    onClearFilter,\n    onClearDateFilter,\n    onCalendarUpdate,\n    onTextFilterUpdate,\n    onCommitFilters,\n    onExportCsv,\n    onExportJson,\n    onCopyTsv,\n    onCopyMarkdown,\n    onDensityChange,\n    toolbarExtra,\n    customFilters,\n  }: DataTableToolbarProps = $props()\n</script>\n\n<div class={['flex flex-col gap-2 py-3', borderless ? '' : 'border-b px-4'].join(' ')}>\n  <!-- flex-wrap so faceted chips reflow instead of overflowing on narrow tables -->\n  <div class=\"flex flex-wrap items-center gap-2\">\n    <!-- Search only renders when filterColumn maps to an actual column. -->\n    {#if enableSearch && filterColumn && table.getColumn(filterColumn)}\n      <div class=\"relative w-full max-w-xs min-w-[12rem] flex-1 sm:flex-none\">\n        <Search\n          class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\"\n          aria-hidden=\"true\"\n        />\n        <Input\n          class=\"h-8 pl-8\"\n          placeholder={filterPlaceholder}\n          aria-label={filterPlaceholder || 'Search table'}\n          value={(table.getColumn(filterColumn)?.getFilterValue() as string) ?? ''}\n          onValueChange={(v) => onSearch(v)}\n        />\n      </div>\n    {/if}\n\n    <!-- ── INLINE filter mode ── -->\n    {#if filterMode === 'inline'}\n      {#each filters as filter (filter.column)}\n        {#if filter.type === 'multiselect' || filter.type === 'select'}\n          {@const multi = getMultiSelectValue(filter.column)}\n          <Popover>\n            <PopoverTrigger>\n              {#snippet child({ props })}\n                <Button\n                  {...props}\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class={[\n                    'h-8 border-dashed',\n                    multi.length > 0 ? 'border-primary/40 bg-primary/5 border-solid' : '',\n                  ].join(' ')}\n                >\n                  <Plus class=\"size-4\" aria-hidden=\"true\" />\n                  {filter.label}\n                  {#if multi.length > 0}\n                    <Separator orientation=\"vertical\" class=\"mx-1 h-4\" />\n                    <div class=\"flex gap-1\">\n                      {#if multi.length > 2}\n                        <Badge variant=\"secondary\" class=\"rounded-sm px-1 font-normal\">\n                          {multi.length} selected\n                        </Badge>\n                      {:else}\n                        {#each getFilterSelectedLabels(filter) as label (label)}\n                          <Badge variant=\"secondary\" class=\"rounded-sm px-1 font-normal\">{label}</Badge>\n                        {/each}\n                      {/if}\n                    </div>\n                  {/if}\n                </Button>\n              {/snippet}\n            </PopoverTrigger>\n            <PopoverContent class=\"w-52 p-0\" align=\"start\">\n              <Command>\n                <CommandInput placeholder={`Search ${filter.label.toLowerCase()}...`} />\n                <CommandList>\n                  <CommandEmpty>No results.</CommandEmpty>\n                  <CommandGroup>\n                    {#each filter.options ?? [] as rawOpt (resolveOption(rawOpt).value)}\n                      {@const opt = resolveOption(rawOpt)}\n                      {@const OptIcon = opt.icon}\n                      <CommandItem value={opt.label} onSelect={() => onToggleMultiselect(filter.column, opt.value)}>\n                        <div\n                          class={[\n                            'border-primary flex size-4 shrink-0 items-center justify-center rounded-sm border',\n                            multi.includes(opt.value)\n                              ? 'bg-primary text-primary-foreground'\n                              : 'opacity-50 [&_svg]:invisible',\n                          ].join(' ')}\n                        >\n                          <Check class=\"size-3\" />\n                        </div>\n                        {#if OptIcon}<OptIcon class=\"text-muted-foreground size-4\" />{/if}\n                        <span>{opt.label}</span>\n                      </CommandItem>\n                    {/each}\n                  </CommandGroup>\n                  {#if multi.length > 0}\n                    <CommandSeparator />\n                    <CommandGroup>\n                      <CommandItem\n                        value=\"__clear__\"\n                        class=\"justify-center text-center\"\n                        onSelect={() => onClearFilter(filter)}\n                      >\n                        Clear filter\n                      </CommandItem>\n                    </CommandGroup>\n                  {/if}\n                </CommandList>\n              </Command>\n            </PopoverContent>\n          </Popover>\n        {:else if filter.type === 'date'}\n          {@const dr = getDateRangeValue(filter.column)}\n          {@const hasDate = !!(dr.from || dr.to)}\n          <Popover>\n            <PopoverTrigger>\n              {#snippet child({ props })}\n                <Button\n                  {...props}\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class={['h-8 border-dashed', hasDate ? 'border-primary/40 bg-primary/5 border-solid' : ''].join(' ')}\n                >\n                  <CalendarIcon class=\"size-4\" aria-hidden=\"true\" />\n                  {filter.label}\n                  {#if hasDate}\n                    <Separator orientation=\"vertical\" class=\"mx-1 h-4\" />\n                    <Badge variant=\"secondary\" class=\"rounded-sm px-1 font-normal\">\n                      {formatDateRange(filter.column)}\n                    </Badge>\n                  {/if}\n                </Button>\n              {/snippet}\n            </PopoverTrigger>\n            <PopoverContent class=\"w-auto p-0\" align=\"start\">\n              <DataTableRangeCalendar\n                value={getCalendarModel(filter.column)}\n                numberOfMonths={2}\n                onValueChange={(range) => onCalendarUpdate(filter.column, range)}\n              />\n              {#if hasDate}\n                <div class=\"border-t p-2\">\n                  <Button variant=\"ghost\" size=\"sm\" class=\"h-7 w-full text-xs\" onclick={() => onClearDateFilter(filter)}>\n                    Clear dates\n                  </Button>\n                </div>\n              {/if}\n            </PopoverContent>\n          </Popover>\n        {:else if filter.type === 'text'}\n          {@const textValue = table.getColumn(filter.column)?.getFilterValue() as string | undefined}\n          <Popover>\n            <PopoverTrigger>\n              {#snippet child({ props })}\n                <Button\n                  {...props}\n                  variant=\"outline\"\n                  size=\"sm\"\n                  class={['h-8 border-dashed', textValue ? 'border-primary/40 bg-primary/5 border-solid' : ''].join(\n                    ' ',\n                  )}\n                >\n                  <Plus class=\"size-4\" aria-hidden=\"true\" />\n                  {filter.label}\n                  {#if textValue}\n                    <Separator orientation=\"vertical\" class=\"mx-1 h-4\" />\n                    <Badge variant=\"secondary\" class=\"rounded-sm px-1 font-normal\">{textValue}</Badge>\n                  {/if}\n                </Button>\n              {/snippet}\n            </PopoverTrigger>\n            <PopoverContent class=\"w-60 p-3\" align=\"start\">\n              <div class=\"space-y-2\">\n                <p class=\"text-sm font-medium\">{filter.label}</p>\n                <Input\n                  placeholder={`Filter by ${filter.label.toLowerCase()}...`}\n                  value={(table.getColumn(filter.column)?.getFilterValue() as string) ?? ''}\n                  class=\"h-8 text-sm\"\n                  onValueChange={(v) => onTextFilterUpdate(filter.column, v || undefined)}\n                />\n              </div>\n            </PopoverContent>\n          </Popover>\n        {/if}\n      {/each}\n    {/if}\n\n    <!-- ── MODAL filter mode (Sheet from the right) ── -->\n    {#if filterMode === 'modal'}\n      <Button\n        variant=\"outline\"\n        size=\"sm\"\n        class={[\n          'h-8',\n          activeFilterCount > 0 ? 'border-primary/40 bg-primary/5 text-primary hover:bg-primary/10' : '',\n        ].join(' ')}\n        onclick={onOpenFilterSheet}\n      >\n        <SlidersHorizontal class=\"size-4\" aria-hidden=\"true\" />\n        Filters\n        {#if activeFilterCount > 0}\n          <Badge class=\"bg-primary text-primary-foreground ml-0.5 size-5 rounded-full p-0 text-xs font-semibold\">\n            {activeFilterCount}\n          </Badge>\n        {/if}\n      </Button>\n    {/if}\n\n    <!-- ── POPOVER filter mode ── -->\n    {#if filterMode === 'popover' && filters.length > 0}\n      <DataTableFilterPopover\n        {table}\n        {filters}\n        {activeFilterCount}\n        {isAnyFilterActive}\n        {isServerSide}\n        {getMultiSelectValue}\n        {getDateRangeValue}\n        {formatDateRange}\n        {getCalendarModel}\n        onCommitDraft={(d) => onCommitFilters(d)}\n        onClearAll={onClearAllFilters}\n        {customFilters}\n      />\n    {/if}\n\n    <!-- Inline custom filters (when filterMode === 'inline') -->\n    {#if filterMode === 'inline'}\n      {@render customFilters?.()}\n    {/if}\n\n    <!-- Toolbar extras (e.g. group-by selector, density toggle) -->\n    {@render toolbarExtra?.()}\n\n    <!-- Reset button -->\n    {#if isAnyFilterActive}\n      <Button variant=\"ghost\" size=\"sm\" class=\"h-8 px-2\" onclick={onClearAllFilters}>\n        Reset\n        <X class=\"size-4\" aria-hidden=\"true\" />\n      </Button>\n    {/if}\n\n    <!-- Right cluster: export / density / columns -->\n    {#if enableExport || enableDensityToggle || enableColumnVisibility}\n      <div class=\"ml-auto flex items-center gap-2\">\n        {#if enableExport}\n          <DropdownMenu>\n            <DropdownMenuTrigger>\n              {#snippet child({ props })}\n                <Button {...props} variant=\"outline\" size=\"sm\" class=\"h-8\">\n                  <Download class=\"size-4\" />\n                  Export\n                  <ChevronDown class=\"size-4 opacity-60\" />\n                </Button>\n              {/snippet}\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem onSelect={onExportCsv}>Export as CSV</DropdownMenuItem>\n              <DropdownMenuItem onSelect={onExportJson}>Export as JSON</DropdownMenuItem>\n              <DropdownMenuItem onSelect={() => onCopyTsv?.()}>Copy as TSV (Excel / Sheets)</DropdownMenuItem>\n              <DropdownMenuItem onSelect={() => onCopyMarkdown?.()}>Copy as Markdown</DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        {/if}\n\n        {#if enableDensityToggle}\n          <DropdownMenu>\n            <DropdownMenuTrigger>\n              {#snippet child({ props })}\n                <Button {...props} variant=\"outline\" size=\"sm\" class=\"h-8\" aria-label={`Row density: ${density}`}>\n                  <Rows3 class=\"size-4\" aria-hidden=\"true\" />\n                  <span class=\"capitalize\">{density}</span>\n                </Button>\n              {/snippet}\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuRadioGroup\n                value={density}\n                onValueChange={(v) => onDensityChange(v as 'compact' | 'cozy' | 'comfortable')}\n              >\n                <DropdownMenuRadioItem value=\"compact\">Compact</DropdownMenuRadioItem>\n                <DropdownMenuRadioItem value=\"cozy\">Cozy</DropdownMenuRadioItem>\n                <DropdownMenuRadioItem value=\"comfortable\">Comfortable</DropdownMenuRadioItem>\n              </DropdownMenuRadioGroup>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        {/if}\n\n        {#if enableColumnVisibility}\n          <DropdownMenu>\n            <DropdownMenuTrigger>\n              {#snippet child({ props })}\n                <Button {...props} variant=\"outline\" size=\"sm\" class=\"h-8\">\n                  <Columns3 class=\"size-4\" aria-hidden=\"true\" />\n                  View\n                </Button>\n              {/snippet}\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\" class=\"w-44\">\n              {#each table.getAllColumns().filter((column) => column.getCanHide()) as column (column.id)}\n                <DropdownMenuCheckboxItem\n                  class=\"capitalize\"\n                  checked={column.getIsVisible()}\n                  onCheckedChange={(value) => column.toggleVisibility(!!value)}\n                >\n                  {column.id}\n                </DropdownMenuCheckboxItem>\n              {/each}\n            </DropdownMenuContent>\n          </DropdownMenu>\n        {/if}\n      </div>\n    {/if}\n  </div>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTableToolbar.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/DataTableRangeCalendar.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { DateRange } from '$lib/components/ui/range-calendar'\n\n  export interface DataTableRangeCalendarProps {\n    value: DateRange | undefined\n    onValueChange: (value: DateRange | undefined) => void\n    numberOfMonths?: number\n    class?: string\n  }\n</script>\n\n<script lang=\"ts\">\n  import { RangeCalendar } from '$lib/components/ui/range-calendar'\n\n  // Internal bridge used by every DataTable filter surface. The Svelte\n  // RangeCalendar keeps an internal copy of the last committed range, so a\n  // filter cleared from outside (Clear / Reset / cancel-restore) would keep\n  // showing the old selection. Remount the calendar whenever the model is\n  // cleared so it always reflects the column filter.\n  let { value, onValueChange, numberOfMonths = 1, class: className }: DataTableRangeCalendarProps = $props()\n\n  let epoch = $state(0)\n  let hadValue = false\n  $effect.pre(() => {\n    const hasValue = value !== undefined\n    if (hadValue && !hasValue) epoch++\n    hadValue = hasValue\n  })\n</script>\n\n{#key epoch}\n  <RangeCalendar {value} {numberOfMonths} class={className} onValueChange={(range) => onValueChange(range)} />\n{/key}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/DataTableRangeCalendar.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/FlexRender.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { ColumnDefTemplate } from '@tanstack/table-core'\n\n  export interface FlexRenderProps<TContext extends object> {\n    /** `columnDef.header` / `columnDef.cell` / `columnDef.footer`. */\n    content: ColumnDefTemplate<TContext> | undefined\n    /** `header.getContext()` / `cell.getContext()`. */\n    context: TContext\n  }\n</script>\n\n<script lang=\"ts\" generics=\"TContext extends object\">\n  import { RenderComponentConfig, RenderSnippetConfig } from './render-helpers'\n\n  // Svelte twin of @tanstack/react-table's `flexRender`: strings render as\n  // text; functions are called with the context and may return a primitive,\n  // `renderComponent(...)` or `renderSnippet(...)`.\n  let { content, context }: FlexRenderProps<TContext> = $props()\n\n  const result = $derived(typeof content === 'function' ? (content as (ctx: TContext) => unknown)(context) : content)\n</script>\n\n{#if result instanceof RenderComponentConfig}\n  {@const Comp = result.component}\n  <Comp {...result.props} />\n{:else if result instanceof RenderSnippetConfig}\n  {@render result.snippet(result.params)}\n{:else if result !== null && result !== undefined && result !== false}\n  {result}\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/FlexRender.svelte"
    },
    {
      "path": "packages/registry-svelte/components/data-table/render-helpers.ts",
      "content": "/**\n * Cell / header renderers for Svelte. React's column defs return JSX from\n * `header` / `cell`; in Svelte a column def returns one of these configs (or\n * a plain string / number) and `<FlexRender>` mounts it:\n *\n *   cell: ({ row }) => renderComponent(StatusBadge, { status: row.original.status })\n *   cell: ({ row }) => renderSnippet(statusCell, row.original)\n *   header: ({ column }) => renderComponent(DataTableColumnHeader, { column, label: 'Name' })\n */\nimport type { Component, ComponentProps, Snippet } from 'svelte'\n\nexport class RenderComponentConfig<TComponent extends Component<any>> {\n  component: TComponent\n  props: ComponentProps<TComponent> | Record<string, never>\n  constructor(component: TComponent, props: ComponentProps<TComponent> | Record<string, never> = {}) {\n    this.component = component\n    this.props = props\n  }\n}\n\nexport class RenderSnippetConfig<TProps> {\n  snippet: Snippet<[TProps]>\n  params: TProps\n  constructor(snippet: Snippet<[TProps]>, params: TProps) {\n    this.snippet = snippet\n    this.params = params\n  }\n}\n\n/** Mount a Svelte component as a cell / header. */\nexport function renderComponent<TComponent extends Component<any>>(\n  component: TComponent,\n  props: ComponentProps<TComponent>,\n): RenderComponentConfig<TComponent> {\n  return new RenderComponentConfig(component, props)\n}\n\n/** Render a snippet (declared at the top level of your component) as a cell / header. */\nexport function renderSnippet<TProps>(snippet: Snippet<[TProps]>, params: TProps): RenderSnippetConfig<TProps> {\n  return new RenderSnippetConfig(snippet, params)\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/render-helpers.ts"
    },
    {
      "path": "packages/registry-svelte/components/data-table/table.svelte.ts",
      "content": "/**\n * Svelte 5 adapter for `@tanstack/table-core` (the pattern shadcn-svelte\n * ships). `@tanstack/svelte-table` v8 is store-based (Svelte 3/4 era), so we\n * wrap `createTable()` directly: options are merged through lazy getters, and\n * the table's own internal state (anything not controlled via `options.state`,\n * e.g. column sizing) lives in a `$state` so reads made while rendering are\n * tracked by Svelte's fine-grained reactivity.\n */\nimport {\n  createTable,\n  type RowData,\n  type TableOptions,\n  type TableOptionsResolved,\n  type TableState,\n} from '@tanstack/table-core'\n\nexport function createSvelteTable<TData extends RowData>(options: TableOptions<TData>) {\n  const resolvedOptions: TableOptionsResolved<TData> = mergeObjects(\n    {\n      state: {},\n      onStateChange() {},\n      renderFallbackValue: null,\n      mergeOptions: (defaultOptions: TableOptions<TData>, opts: Partial<TableOptions<TData>>) =>\n        mergeObjects(defaultOptions, opts),\n    },\n    options,\n  )\n\n  const table = createTable(resolvedOptions)\n  let state = $state<Partial<TableState>>(table.initialState)\n\n  function updateOptions() {\n    table.setOptions((prev) =>\n      mergeObjects(prev, options, {\n        // Thunk (not the value) so every read goes through the `state` signal:\n        // reassigning `state` then invalidates renders that read internal\n        // slices such as columnSizing / columnSizingInfo.\n        state: mergeObjects(() => state, options.state || {}),\n        onStateChange: (updater: unknown) => {\n          if (updater instanceof Function) state = updater(state)\n          else state = mergeObjects(state, updater as Partial<TableState>)\n          options.onStateChange?.(updater as never)\n        },\n      }),\n    )\n  }\n\n  updateOptions()\n\n  $effect.pre(() => {\n    updateOptions()\n  })\n\n  return table\n}\n\ntype MaybeThunk<T extends object> = T | (() => T | null | undefined)\ntype Intersection<T extends readonly unknown[]> = (T extends [infer H, ...infer R]\n  ? H & Intersection<R>\n  : unknown) & {}\n\n/**\n * Merge objects into one whose properties are lazy getters resolving\n * right-to-left (later sources win, `undefined` falls through). Keeping the\n * getters lazy is what lets reactive `$state`/`$props` reads flow through\n * TanStack's option object without being snapshotted.\n */\nexport function mergeObjects<Sources extends readonly MaybeThunk<object>[]>(\n  ...sources: Sources\n): Intersection<{ [K in keyof Sources]: Sources[K] extends () => infer R ? NonNullable<R> : Sources[K] }> {\n  const target = {}\n  for (let i = 0; i < sources.length; i++) {\n    let source = sources[i]\n    if (typeof source === 'function') source = source()\n    if (source) {\n      const descriptors = Object.getOwnPropertyDescriptors(source)\n      for (const key in descriptors) {\n        if (key in target) continue\n        Object.defineProperty(target, key, {\n          enumerable: true,\n          get() {\n            for (let j = sources.length - 1; j >= 0; j--) {\n              let s = sources[j]\n              if (typeof s === 'function') s = s()\n              const v = ((s || {}) as Record<string, unknown>)[key]\n              if (v !== undefined) return v\n            }\n          },\n        })\n      }\n    }\n  }\n  return target as never\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/table.svelte.ts"
    },
    {
      "path": "packages/registry-svelte/components/data-table/types.ts",
      "content": "import type { Component } from 'svelte'\n\nexport interface FilterOption {\n  value: string\n  label: string\n  icon?: Component<{ class?: string }>\n}\n\nexport interface FilterDefinition {\n  column: string\n  label: string\n  type: 'text' | 'select' | 'multiselect' | 'date'\n  options?: (string | FilterOption)[]\n}\n\nexport function resolveOption(opt: string | FilterOption): FilterOption {\n  if (typeof opt === 'string') return { value: opt, label: opt }\n  return opt\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/types.ts"
    },
    {
      "path": "packages/registry-svelte/components/data-table/date-utils.ts",
      "content": "import type { DateRange } from '$lib/components/ui/range-calendar'\n\n/**\n * ISO-string ↔ native Date helpers shared across the DataTable filter\n * surfaces. The Svelte RangeCalendar speaks native `Date` (as a\n * `{ start, end }` range), so -- like the React twin -- we keep these tiny\n * converters local instead of pulling `@internationalized/date`. Column\n * filter values are still stored as `YYYY-MM-DD` ISO strings on the wire,\n * identical to Vue/React, so server-side payloads match 1:1.\n */\nexport function isoToDate(str: string | undefined): Date | undefined {\n  if (!str) return undefined\n  const [y, m, d] = str.split('-').map(Number)\n  if (y === undefined || m === undefined || d === undefined) return undefined\n  return new Date(y, m - 1, d)\n}\n\nexport function dateToIso(date: Date | undefined): string {\n  if (!date) return ''\n  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`\n}\n\n/** `{ from, to }` ISO filter value -> RangeCalendar model (`undefined` when empty). */\nexport function isoRangeToCalendar(v: { from?: string; to?: string } | undefined): DateRange | undefined {\n  if (!v?.from && !v?.to) return undefined\n  const from = isoToDate(v.from)\n  const to = isoToDate(v.to)\n  // The Svelte range model needs a start; an until-only filter anchors on `to`.\n  if (!from) return to ? { start: to, end: to } : undefined\n  return { start: from, end: to }\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/date-utils.ts"
    },
    {
      "path": "packages/registry-svelte/components/data-table/index.ts",
      "content": "export {\n  default as DataTable,\n  type DataTableProps,\n  type DataTableHandle,\n  type DataTableState,\n} from './DataTable.svelte'\nexport { default as DataTableColumnHeader, type DataTableColumnHeaderProps } from './DataTableColumnHeader.svelte'\nexport { default as DataTableToolbar, type DataTableToolbarProps } from './DataTableToolbar.svelte'\nexport { default as DataTableFilterSheet, type DataTableFilterSheetProps } from './DataTableFilterSheet.svelte'\nexport { default as DataTableFilterPopover, type DataTableFilterPopoverProps } from './DataTableFilterPopover.svelte'\nexport { default as DataTablePagination, type DataTablePaginationProps } from './DataTablePagination.svelte'\nexport type { FilterDefinition, FilterOption } from './types'\n// Svelte-only: cell / header rendering (React gets these from JSX + flexRender).\nexport { default as FlexRender, type FlexRenderProps } from './FlexRender.svelte'\nexport { renderComponent, renderSnippet, RenderComponentConfig, RenderSnippetConfig } from './render-helpers'\nexport { createSvelteTable } from './table.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/data-table/index.ts"
    }
  ],
  "dependencies": [
    "@tanstack/table-core",
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/badge.json",
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/command.json",
    "https://uipkge.dev/r/svelte/dropdown-menu.json",
    "https://uipkge.dev/r/svelte/input.json",
    "https://uipkge.dev/r/svelte/label.json",
    "https://uipkge.dev/r/svelte/popover.json",
    "https://uipkge.dev/r/svelte/range-calendar.json",
    "https://uipkge.dev/r/svelte/select.json",
    "https://uipkge.dev/r/svelte/separator.json",
    "https://uipkge.dev/r/svelte/sheet.json",
    "https://uipkge.dev/r/svelte/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"
  ]
}