{
  "name": "data-table",
  "type": "registry:block",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "@tanstack/react-table",
    "lucide-react",
    "react"
  ],
  "registryDependencies": [
    "button",
    "checkbox",
    "command",
    "dialog",
    "input",
    "popover",
    "select",
    "separator",
    "table"
  ],
  "files": [
    {
      "path": "./items/blocks/data-table/component.tsx",
      "type": "registry:block",
      "target": "./components/block/shuip/data-table.tsx",
      "content": "'use client';\n\nimport {\n  closestCenter,\n  DndContext,\n  type DragEndEvent,\n  KeyboardSensor,\n  PointerSensor,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\nimport {\n  arrayMove,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\nimport {\n  type Column,\n  type ColumnDef,\n  type ColumnFiltersState,\n  type ColumnPinningState,\n  type FilterFn,\n  flexRender,\n  getCoreRowModel,\n  getFacetedRowModel,\n  getFacetedUniqueValues,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  type OnChangeFn,\n  type PaginationState,\n  type RowData,\n  type RowSelectionState,\n  type SortingState,\n  type Table,\n  useReactTable,\n  type VisibilityState,\n} from '@tanstack/react-table';\nimport {\n  ArrowDown,\n  ArrowDownUp,\n  ArrowUp,\n  Bookmark,\n  CheckIcon,\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n  ChevronsUpDown,\n  EyeOff,\n  GripVertical,\n  Inbox,\n  ListFilter,\n  Loader2,\n  Plus,\n  PlusCircle,\n  Settings2,\n  Trash2,\n  X,\n} from 'lucide-react';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from '@/components/ui/command';\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from '@/components/ui/dialog';\nimport { Input } from '@/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { Separator } from '@/components/ui/separator';\nimport { TableBody, TableCell, TableHead, TableHeader, Table as TableRoot, TableRow } from '@/components/ui/table';\nimport { cn } from '@/lib/utils';\n\nexport type DataTableFilterOption = {\n  label: string;\n  value: string;\n  icon?: React.ComponentType<{ className?: string }>;\n  count?: number;\n};\n\ndeclare module '@tanstack/react-table' {\n  interface ColumnMeta<TData extends RowData, TValue> {\n    label?: string;\n    placeholder?: string;\n    variant?: 'text' | 'number' | 'date' | 'select' | 'multiSelect';\n    options?: DataTableFilterOption[];\n    icon?: React.ComponentType<{ className?: string }>;\n  }\n}\n\nexport type FilterVariant = NonNullable<DataTableColumnMetaVariant>;\ntype DataTableColumnMetaVariant = 'text' | 'number' | 'date' | 'select' | 'multiSelect';\n\nexport type FilterOperator =\n  | 'contains'\n  | 'notContains'\n  | 'is'\n  | 'isNot'\n  | 'isEmpty'\n  | 'isNotEmpty'\n  | 'eq'\n  | 'ne'\n  | 'gt'\n  | 'lt'\n  | 'gte'\n  | 'lte'\n  | 'before'\n  | 'after'\n  | 'onOrBefore'\n  | 'onOrAfter'\n  | 'isAnyOf'\n  | 'isNoneOf';\n\nexport type FilterCondition = { operator: FilterOperator; value: unknown };\n\nconst OPERATOR_LABELS: Record<FilterOperator, string> = {\n  contains: 'contains',\n  notContains: 'does not contain',\n  is: 'is',\n  isNot: 'is not',\n  isEmpty: 'is empty',\n  isNotEmpty: 'is not empty',\n  eq: '=',\n  ne: '≠',\n  gt: '>',\n  lt: '<',\n  gte: '≥',\n  lte: '≤',\n  before: 'is before',\n  after: 'is after',\n  onOrBefore: 'is on or before',\n  onOrAfter: 'is on or after',\n  isAnyOf: 'is any of',\n  isNoneOf: 'is none of',\n};\n\nconst OPERATORS_BY_VARIANT: Record<FilterVariant, FilterOperator[]> = {\n  text: ['contains', 'notContains', 'is', 'isNot', 'isEmpty', 'isNotEmpty'],\n  number: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte', 'isEmpty', 'isNotEmpty'],\n  date: ['is', 'before', 'after', 'onOrBefore', 'onOrAfter', 'isEmpty'],\n  select: ['is', 'isNot', 'isAnyOf', 'isNoneOf'],\n  multiSelect: ['isAnyOf', 'isNoneOf', 'is', 'isNot'],\n};\n\nconst MULTI_VALUE_OPERATORS: FilterOperator[] = ['isAnyOf', 'isNoneOf'];\n\nfunction operatorTakesValue(operator: FilterOperator): boolean {\n  return operator !== 'isEmpty' && operator !== 'isNotEmpty';\n}\n\nfunction isFilterCondition(value: unknown): value is FilterCondition {\n  return typeof value === 'object' && value !== null && 'operator' in value;\n}\n\nfunction conditionIsEmpty(condition: FilterCondition): boolean {\n  if (!operatorTakesValue(condition.operator)) return false;\n  const { value } = condition;\n  return value == null || value === '' || (Array.isArray(value) && value.length === 0);\n}\n\nexport const dataTableFilterFn: FilterFn<unknown> = (row, columnId, filterValue) => {\n  if (Array.isArray(filterValue)) {\n    const cell = row.getValue(columnId);\n    return filterValue.length === 0 || filterValue.includes(cell);\n  }\n  if (!isFilterCondition(filterValue)) return true;\n  const { operator, value } = filterValue;\n  if (conditionIsEmpty(filterValue)) return true;\n\n  const cell = row.getValue(columnId);\n  const text = String(cell ?? '').toLowerCase();\n  const target = String(value ?? '').toLowerCase();\n\n  switch (operator) {\n    case 'isEmpty':\n      return cell == null || cell === '';\n    case 'isNotEmpty':\n      return !(cell == null || cell === '');\n    case 'contains':\n      return text.includes(target);\n    case 'notContains':\n      return !text.includes(target);\n    case 'is':\n      return text === target;\n    case 'isNot':\n      return text !== target;\n    case 'eq':\n      return Number(cell) === Number(value);\n    case 'ne':\n      return Number(cell) !== Number(value);\n    case 'gt':\n      return Number(cell) > Number(value);\n    case 'lt':\n      return Number(cell) < Number(value);\n    case 'gte':\n      return Number(cell) >= Number(value);\n    case 'lte':\n      return Number(cell) <= Number(value);\n    case 'before':\n      return new Date(String(cell)) < new Date(String(value));\n    case 'after':\n      return new Date(String(cell)) > new Date(String(value));\n    case 'onOrBefore':\n      return new Date(String(cell)) <= new Date(String(value));\n    case 'onOrAfter':\n      return new Date(String(cell)) >= new Date(String(value));\n    case 'isAnyOf':\n      return Array.isArray(value) ? value.includes(cell) : true;\n    case 'isNoneOf':\n      return Array.isArray(value) ? !value.includes(cell) : true;\n    default:\n      return true;\n  }\n};\n\nexport type UseDataTableProps<TData> = {\n  data: TData[];\n  columns: ColumnDef<TData, unknown>[];\n  pageCount?: number;\n  getRowId?: (row: TData, index: number) => string;\n  enableRowSelection?: boolean;\n  enableColumnPinning?: boolean;\n  initialState?: {\n    pagination?: PaginationState;\n    sorting?: SortingState;\n    columnVisibility?: VisibilityState;\n    columnPinning?: ColumnPinningState;\n  };\n  state?: {\n    pagination?: PaginationState;\n    sorting?: SortingState;\n    columnFilters?: ColumnFiltersState;\n    globalFilter?: string;\n  };\n  onPaginationChange?: OnChangeFn<PaginationState>;\n  onSortingChange?: OnChangeFn<SortingState>;\n  onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;\n  onGlobalFilterChange?: OnChangeFn<string>;\n};\n\nexport function useDataTable<TData>(props: UseDataTableProps<TData>) {\n  const {\n    data,\n    columns,\n    pageCount,\n    getRowId,\n    enableRowSelection = false,\n    enableColumnPinning = false,\n    initialState,\n  } = props;\n\n  const manual = pageCount != null;\n\n  const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []);\n  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);\n  const [globalFilter, setGlobalFilter] = React.useState('');\n  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialState?.columnVisibility ?? {});\n  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});\n  const [pagination, setPagination] = React.useState<PaginationState>(\n    initialState?.pagination ?? { pageIndex: 0, pageSize: 10 },\n  );\n  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>(() => ({\n    left: initialState?.columnPinning?.left ?? [],\n    right: initialState?.columnPinning?.right ?? [],\n  }));\n\n  const table = useReactTable({\n    data,\n    columns,\n    pageCount: pageCount ?? undefined,\n    defaultColumn: { filterFn: dataTableFilterFn },\n    state: {\n      sorting: props.state?.sorting ?? sorting,\n      columnFilters: props.state?.columnFilters ?? columnFilters,\n      globalFilter: props.state?.globalFilter ?? globalFilter,\n      columnVisibility,\n      rowSelection,\n      pagination: props.state?.pagination ?? pagination,\n      columnPinning,\n    },\n    enableRowSelection,\n    enableColumnPinning,\n    getRowId,\n    manualPagination: manual,\n    manualSorting: manual,\n    manualFiltering: manual,\n    onSortingChange: props.onSortingChange ?? setSorting,\n    onColumnFiltersChange: props.onColumnFiltersChange ?? setColumnFilters,\n    onGlobalFilterChange: props.onGlobalFilterChange ?? setGlobalFilter,\n    onColumnVisibilityChange: setColumnVisibility,\n    onRowSelectionChange: setRowSelection,\n    onPaginationChange: props.onPaginationChange ?? setPagination,\n    onColumnPinningChange: setColumnPinning,\n    getCoreRowModel: getCoreRowModel(),\n    ...(manual\n      ? {}\n      : {\n          getSortedRowModel: getSortedRowModel(),\n          getFilteredRowModel: getFilteredRowModel(),\n          getPaginationRowModel: getPaginationRowModel(),\n          getFacetedRowModel: getFacetedRowModel(),\n          getFacetedUniqueValues: getFacetedUniqueValues(),\n        }),\n  });\n\n  return { table };\n}\n\nfunction getColumnStyles<TData>(column: Column<TData>): React.CSSProperties {\n  const pinned = column.getIsPinned();\n  return {\n    width: column.getSize(),\n    ...(pinned\n      ? {\n          position: 'sticky',\n          left: pinned === 'left' ? column.getStart('left') : undefined,\n          right: pinned === 'right' ? column.getAfter('right') : undefined,\n          zIndex: 1,\n        }\n      : {}),\n  };\n}\n\nfunction getPinClass<TData>(column: Column<TData>): string | undefined {\n  return column.getIsPinned() ? 'bg-background' : undefined;\n}\n\nconst skeletonBar = <div className='h-5 w-full animate-pulse rounded bg-muted' />;\n\nconst shimmerBar = (\n  <div className='relative h-5 w-full overflow-hidden rounded bg-muted'>\n    <div\n      className='absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-foreground/10 to-transparent motion-reduce:hidden'\n      style={{ animation: 'shuip-dt-shimmer 1.5s infinite' }}\n    />\n  </div>\n);\n\nconst shimmerKeyframes = <style>{'@keyframes shuip-dt-shimmer{100%{transform:translateX(100%)}}'}</style>;\n\nexport type DataTableProps<TData> = {\n  table: Table<TData>;\n  isLoading?: boolean;\n  loadingVariant?: 'skeleton' | 'overlay' | 'shimmer';\n  emptyState?: React.ReactNode;\n  onRowClick?: (row: TData) => void;\n  className?: string;\n};\n\nexport function DataTable<TData>({\n  table,\n  isLoading,\n  loadingVariant = 'skeleton',\n  emptyState,\n  onRowClick,\n  className,\n}: DataTableProps<TData>) {\n  const columnCount = table.getVisibleLeafColumns().length;\n  const rows = table.getRowModel().rows;\n  const showSkeleton = isLoading && loadingVariant !== 'overlay';\n  const showOverlay = isLoading && loadingVariant === 'overlay';\n\n  return (\n    <div className={cn('relative rounded-md border', className)}>\n      <TableRoot\n        className={cn('table-fixed', showOverlay && 'pointer-events-none opacity-60')}\n        style={{ minWidth: table.getTotalSize() }}\n      >\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => (\n                <TableHead\n                  key={header.id}\n                  colSpan={header.colSpan}\n                  style={getColumnStyles(header.column)}\n                  className={getPinClass(header.column)}\n                >\n                  {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {showSkeleton ? (\n            Array.from({ length: table.getState().pagination.pageSize }).map((_, rowIndex) => (\n              <TableRow key={rowIndex}>\n                {table.getVisibleLeafColumns().map((column) => (\n                  <TableCell key={column.id} style={getColumnStyles(column)} className={getPinClass(column)}>\n                    {loadingVariant === 'shimmer' ? shimmerBar : skeletonBar}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))\n          ) : rows.length ? (\n            rows.map((row) => (\n              <TableRow\n                key={row.id}\n                data-state={row.getIsSelected() ? 'selected' : undefined}\n                onClick={onRowClick ? () => onRowClick(row.original) : undefined}\n                className={onRowClick ? 'cursor-pointer' : undefined}\n              >\n                {row.getVisibleCells().map((cell) => (\n                  <TableCell\n                    key={cell.id}\n                    style={getColumnStyles(cell.column)}\n                    className={cn('overflow-hidden', getPinClass(cell.column))}\n                  >\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))\n          ) : (\n            <TableRow>\n              <TableCell colSpan={columnCount} className='h-24 text-center'>\n                {emptyState ?? 'No results.'}\n              </TableCell>\n            </TableRow>\n          )}\n        </TableBody>\n      </TableRoot>\n      {showOverlay && (\n        <div className='absolute inset-0 z-10 flex items-center justify-center bg-background/60 backdrop-blur-[1px]'>\n          <Loader2 className='size-5 animate-spin text-muted-foreground' />\n        </div>\n      )}\n      {loadingVariant === 'shimmer' && shimmerKeyframes}\n    </div>\n  );\n}\n\nexport type DataTableEmptyProps = {\n  variant?: 'text' | 'illustrated' | 'with-action';\n  title?: string;\n  description?: string;\n  icon?: React.ComponentType<{ className?: string }>;\n  action?: React.ReactNode;\n};\n\nexport function DataTableEmpty({\n  variant = 'text',\n  title = 'No results',\n  description,\n  icon: Icon = Inbox,\n  action,\n}: DataTableEmptyProps) {\n  if (variant === 'text') {\n    return <span className='text-muted-foreground text-sm'>{title}</span>;\n  }\n\n  return (\n    <div className='flex flex-col items-center justify-center gap-3 py-6 text-center'>\n      <div className='flex size-12 items-center justify-center rounded-full bg-muted/50'>\n        <Icon className='size-6 text-muted-foreground' />\n      </div>\n      <div className='space-y-1'>\n        <p className='text-balance font-medium text-sm'>{title}</p>\n        {description && <p className='mx-auto max-w-xs text-balance text-muted-foreground text-sm'>{description}</p>}\n      </div>\n      {variant === 'with-action' && action}\n    </div>\n  );\n}\n\nexport type DataTableColumnHeaderProps<TData, TValue> = {\n  column: Column<TData, TValue>;\n  title: string;\n  className?: string;\n};\n\nexport function DataTableColumnHeader<TData, TValue>({\n  column,\n  title,\n  className,\n}: DataTableColumnHeaderProps<TData, TValue>) {\n  if (!column.getCanSort() && !column.getCanHide()) {\n    return <div className={className}>{title}</div>;\n  }\n\n  const sorted = column.getIsSorted();\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant='ghost' size='sm' className={cn('-ml-3 h-8 data-[state=open]:bg-accent', className)}>\n          <span>{title}</span>\n          {sorted === 'desc' ? (\n            <ArrowDown className='ml-2 size-4' />\n          ) : sorted === 'asc' ? (\n            <ArrowUp className='ml-2 size-4' />\n          ) : (\n            <ChevronsUpDown className='ml-2 size-4' />\n          )}\n          {sorted && column.getSortIndex() >= 1 && (\n            <span className='ml-1 rounded bg-muted px-1 font-mono text-[10px] text-muted-foreground tabular-nums'>\n              {column.getSortIndex() + 1}\n            </span>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align='start' className='flex w-40 flex-col gap-0.5 p-1'>\n        {column.getCanSort() && (\n          <>\n            <Button variant='ghost' size='sm' className='justify-start' onClick={() => column.toggleSorting(false)}>\n              <ArrowUp className='mr-2 size-3.5 text-muted-foreground/70' /> Asc\n            </Button>\n            <Button variant='ghost' size='sm' className='justify-start' onClick={() => column.toggleSorting(true)}>\n              <ArrowDown className='mr-2 size-3.5 text-muted-foreground/70' /> Desc\n            </Button>\n          </>\n        )}\n        {column.getCanHide() && (\n          <Button variant='ghost' size='sm' className='justify-start' onClick={() => column.toggleVisibility(false)}>\n            <EyeOff className='mr-2 size-3.5 text-muted-foreground/70' /> Hide\n          </Button>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport type DataTableFacetedFilterProps<TData, TValue> = {\n  column?: Column<TData, TValue>;\n  title?: string;\n  options: DataTableFilterOption[];\n};\n\nexport function DataTableFacetedFilter<TData, TValue>({\n  column,\n  title,\n  options,\n}: DataTableFacetedFilterProps<TData, TValue>) {\n  const facets = column?.getFacetedUniqueValues();\n  const selectedValues = new Set((column?.getFilterValue() as string[]) ?? []);\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant='outline' size='sm' className='h-8 border-dashed'>\n          <PlusCircle className='mr-2 size-4' />\n          {title}\n          {selectedValues.size > 0 && (\n            <>\n              <Separator orientation='vertical' className='mx-2 h-4' />\n              <span className='rounded-sm bg-secondary px-1 font-normal text-xs'>{selectedValues.size}</span>\n            </>\n          )}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className='w-48 p-0' align='start'>\n        <Command>\n          <CommandInput placeholder={title} />\n          <CommandList>\n            <CommandEmpty>No results.</CommandEmpty>\n            <CommandGroup>\n              {options.map((option) => {\n                const isSelected = selectedValues.has(option.value);\n                return (\n                  <CommandItem\n                    key={option.value}\n                    onSelect={() => {\n                      const next = new Set(selectedValues);\n                      if (isSelected) next.delete(option.value);\n                      else next.add(option.value);\n                      const filterValues = Array.from(next);\n                      column?.setFilterValue(filterValues.length ? filterValues : undefined);\n                    }}\n                  >\n                    <div\n                      className={cn(\n                        'mr-2 flex size-4 items-center justify-center rounded-sm border border-primary',\n                        isSelected ? 'bg-primary text-primary-foreground' : 'opacity-50 [&_svg]:invisible',\n                      )}\n                    >\n                      <CheckIcon className='size-3.5' />\n                    </div>\n                    {option.icon && <option.icon className='mr-2 size-4 text-muted-foreground' />}\n                    <span>{option.label}</span>\n                    {(facets?.get(option.value) ?? 0) > 0 && (\n                      <span className='ml-auto flex size-4 items-center justify-center font-mono text-xs'>\n                        {facets?.get(option.value)}\n                      </span>\n                    )}\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n            {selectedValues.size > 0 && (\n              <>\n                <CommandSeparator />\n                <CommandGroup>\n                  <CommandItem\n                    onSelect={() => column?.setFilterValue(undefined)}\n                    className='justify-center text-center'\n                  >\n                    Clear filters\n                  </CommandItem>\n                </CommandGroup>\n              </>\n            )}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport function DataTableViewOptions<TData>({ table }: { table: Table<TData> }) {\n  const columns = table\n    .getAllColumns()\n    .filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide());\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant='outline' size='sm' className='ml-auto hidden h-8 lg:flex'>\n          <Settings2 className='mr-2 size-4' /> View\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align='end' className='w-44 p-0'>\n        <Command>\n          <CommandInput placeholder='Search columns...' />\n          <CommandList>\n            <CommandEmpty>No columns.</CommandEmpty>\n            <CommandGroup>\n              {columns.map((column) => (\n                <CommandItem key={column.id} onSelect={() => column.toggleVisibility(!column.getIsVisible())}>\n                  <CheckIcon className={cn('mr-2 size-4', column.getIsVisible() ? 'opacity-100' : 'opacity-0')} />\n                  <span className='truncate'>{column.columnDef.meta?.label ?? column.id}</span>\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\ntype FilterChip = { key: string; label: string; onRemove: () => void };\n\nfunction getFilterChips<TData>(table: Table<TData>): FilterChip[] {\n  const chips: FilterChip[] = [];\n  const globalFilter = table.getState().globalFilter as string;\n  if (globalFilter) {\n    chips.push({ key: 'global', label: `Search: ${globalFilter}`, onRemove: () => table.setGlobalFilter('') });\n  }\n  for (const filter of table.getState().columnFilters) {\n    const column = table.getColumn(filter.id);\n    const meta = column?.columnDef.meta;\n    const columnLabel = meta?.label ?? filter.id;\n    const values = Array.isArray(filter.value) ? (filter.value as string[]) : [filter.value as string];\n    for (const value of values) {\n      const optionLabel = meta?.options?.find((option) => option.value === value)?.label ?? String(value);\n      chips.push({\n        key: `${filter.id}-${value}`,\n        label: `${columnLabel}: ${optionLabel}`,\n        onRemove: () => {\n          const next = values.filter((item) => item !== value);\n          column?.setFilterValue(next.length ? next : undefined);\n        },\n      });\n    }\n  }\n  return chips;\n}\n\nexport type DataTableToolbarProps<TData> = {\n  table: Table<TData>;\n  searchPlaceholder?: string;\n  variant?: 'default' | 'inline-chips' | 'minimal';\n  children?: React.ReactNode;\n};\n\nexport function DataTableToolbar<TData>({\n  table,\n  searchPlaceholder = 'Search...',\n  variant = 'default',\n  children,\n}: DataTableToolbarProps<TData>) {\n  const isFiltered = table.getState().columnFilters.length > 0 || Boolean(table.getState().globalFilter);\n  const showFacets = variant !== 'minimal';\n  const showChips = variant === 'inline-chips';\n  const filterableColumns = table\n    .getAllColumns()\n    .filter((column) => column.getCanFilter() && column.columnDef.meta?.variant);\n  const chips = showChips ? getFilterChips(table) : [];\n\n  const clearAll = () => {\n    table.resetColumnFilters();\n    table.setGlobalFilter('');\n  };\n\n  return (\n    <div className='flex flex-col gap-2'>\n      <div className='flex flex-wrap items-center gap-2'>\n        <Input\n          placeholder={searchPlaceholder}\n          value={(table.getState().globalFilter as string) ?? ''}\n          onChange={(event) => table.setGlobalFilter(event.target.value)}\n          className='h-8 w-40 lg:w-56'\n        />\n        {showFacets &&\n          filterableColumns.map((column) => {\n            const meta = column.columnDef.meta;\n            if ((meta?.variant === 'select' || meta?.variant === 'multiSelect') && meta.options) {\n              return (\n                <DataTableFacetedFilter\n                  key={column.id}\n                  column={column}\n                  title={meta.label ?? column.id}\n                  options={meta.options}\n                />\n              );\n            }\n            return null;\n          })}\n        {isFiltered && !showChips && (\n          <Button variant='ghost' size='sm' className='h-8 px-2' onClick={clearAll}>\n            Reset <X className='ml-2 size-4' />\n          </Button>\n        )}\n        {children}\n        <DataTableViewOptions table={table} />\n      </div>\n      {showChips && chips.length > 0 && (\n        <div className='flex flex-wrap items-center gap-1.5'>\n          {chips.map((chip) => (\n            <span\n              key={chip.key}\n              className='inline-flex items-center gap-1 rounded-full bg-secondary py-0.5 pr-1 pl-2 text-secondary-foreground text-xs'\n            >\n              {chip.label}\n              <button\n                type='button'\n                onClick={chip.onRemove}\n                aria-label={`Remove ${chip.label}`}\n                className='flex size-4 items-center justify-center rounded-full hover:bg-background/60'\n              >\n                <X className='size-3' />\n              </button>\n            </span>\n          ))}\n          <Button variant='ghost' size='sm' className='h-6 px-2 text-xs' onClick={clearAll}>\n            Clear all\n          </Button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction getPaginationRange(currentPage: number, pageCount: number, siblings = 1): (number | 'ellipsis')[] {\n  const totalPageNumbers = siblings * 2 + 5;\n  if (pageCount <= totalPageNumbers) {\n    return Array.from({ length: pageCount }, (_, index) => index + 1);\n  }\n\n  const leftSibling = Math.max(currentPage - siblings, 1);\n  const rightSibling = Math.min(currentPage + siblings, pageCount);\n  const showLeftEllipsis = leftSibling > 2;\n  const showRightEllipsis = rightSibling < pageCount - 1;\n  const edgeCount = 3 + 2 * siblings;\n\n  if (!showLeftEllipsis && showRightEllipsis) {\n    return [...Array.from({ length: edgeCount }, (_, index) => index + 1), 'ellipsis', pageCount];\n  }\n  if (showLeftEllipsis && !showRightEllipsis) {\n    return [1, 'ellipsis', ...Array.from({ length: edgeCount }, (_, index) => pageCount - edgeCount + 1 + index)];\n  }\n  return [\n    1,\n    'ellipsis',\n    ...Array.from({ length: rightSibling - leftSibling + 1 }, (_, index) => leftSibling + index),\n    'ellipsis',\n    pageCount,\n  ];\n}\n\nexport type DataTablePaginationProps<TData> = {\n  table: Table<TData>;\n  pageSizeOptions?: number[];\n  variant?: 'simple' | 'numbered';\n};\n\nexport function DataTablePagination<TData>({\n  table,\n  pageSizeOptions = [10, 20, 30, 40, 50],\n  variant = 'simple',\n}: DataTablePaginationProps<TData>) {\n  const pageSize = table.getState().pagination.pageSize;\n  const pageIndex = table.getState().pagination.pageIndex;\n  const pageCount = Math.max(table.getPageCount(), 1);\n  const options = pageSizeOptions.includes(pageSize)\n    ? pageSizeOptions\n    : [...pageSizeOptions, pageSize].sort((a, b) => a - b);\n\n  return (\n    <div className='flex flex-wrap items-center justify-between gap-4'>\n      <div className='text-muted-foreground text-sm'>\n        {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.\n      </div>\n      <div className='flex flex-wrap items-center gap-4 lg:gap-6'>\n        <div className='flex items-center gap-2'>\n          <p className='font-medium text-sm'>Rows per page</p>\n          <Select value={`${pageSize}`} onValueChange={(value) => table.setPageSize(Number(value))}>\n            <SelectTrigger className='h-8 w-[72px]'>\n              <SelectValue placeholder={pageSize} />\n            </SelectTrigger>\n            <SelectContent side='top'>\n              {options.map((size) => (\n                <SelectItem key={size} value={`${size}`}>\n                  {size}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        {variant === 'numbered' ? (\n          <div className='flex items-center gap-1'>\n            <Button\n              variant='outline'\n              size='icon-sm'\n              onClick={() => table.previousPage()}\n              disabled={!table.getCanPreviousPage()}\n              aria-label='Previous page'\n            >\n              <ChevronLeft className='size-4' />\n            </Button>\n            {getPaginationRange(pageIndex + 1, pageCount).map((page, index) =>\n              page === 'ellipsis' ? (\n                <span\n                  key={`ellipsis-${index}`}\n                  className='flex size-8 items-center justify-center text-muted-foreground text-sm'\n                >\n                  &hellip;\n                </span>\n              ) : (\n                <Button\n                  key={page}\n                  variant={page === pageIndex + 1 ? 'default' : 'ghost'}\n                  size='icon-sm'\n                  className='tabular-nums'\n                  aria-current={page === pageIndex + 1 ? 'page' : undefined}\n                  onClick={() => table.setPageIndex(page - 1)}\n                >\n                  {page}\n                </Button>\n              ),\n            )}\n            <Button\n              variant='outline'\n              size='icon-sm'\n              onClick={() => table.nextPage()}\n              disabled={!table.getCanNextPage()}\n              aria-label='Next page'\n            >\n              <ChevronRight className='size-4' />\n            </Button>\n          </div>\n        ) : (\n          <>\n            <div className='flex items-center font-medium text-sm'>\n              Page {pageIndex + 1} of {pageCount}\n            </div>\n            <div className='flex items-center gap-2'>\n              <Button\n                variant='outline'\n                size='icon-sm'\n                className='hidden lg:flex'\n                onClick={() => table.setPageIndex(0)}\n                disabled={!table.getCanPreviousPage()}\n                aria-label='First page'\n              >\n                <ChevronsLeft className='size-4' />\n              </Button>\n              <Button\n                variant='outline'\n                size='icon-sm'\n                onClick={() => table.previousPage()}\n                disabled={!table.getCanPreviousPage()}\n                aria-label='Previous page'\n              >\n                <ChevronLeft className='size-4' />\n              </Button>\n              <Button\n                variant='outline'\n                size='icon-sm'\n                onClick={() => table.nextPage()}\n                disabled={!table.getCanNextPage()}\n                aria-label='Next page'\n              >\n                <ChevronRight className='size-4' />\n              </Button>\n              <Button\n                variant='outline'\n                size='icon-sm'\n                className='hidden lg:flex'\n                onClick={() => table.setPageIndex(table.getPageCount() - 1)}\n                disabled={!table.getCanNextPage()}\n                aria-label='Last page'\n              >\n                <ChevronsRight className='size-4' />\n              </Button>\n            </div>\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport type DataTableLoadMoreProps = {\n  onLoadMore: () => void;\n  hasMore: boolean;\n  mode?: 'button' | 'infinite';\n  isLoading?: boolean;\n  loaded?: number;\n  total?: number;\n};\n\nexport function DataTableLoadMore({\n  onLoadMore,\n  hasMore,\n  mode = 'button',\n  isLoading = false,\n  loaded,\n  total,\n}: DataTableLoadMoreProps) {\n  const sentinelRef = React.useRef<HTMLDivElement>(null);\n  const onLoadMoreRef = React.useRef(onLoadMore);\n  onLoadMoreRef.current = onLoadMore;\n\n  React.useEffect(() => {\n    if (mode !== 'infinite' || !hasMore || isLoading) return;\n    const node = sentinelRef.current;\n    if (!node) return;\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (entries[0]?.isIntersecting) onLoadMoreRef.current();\n      },\n      { rootMargin: '120px' },\n    );\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [mode, hasMore, isLoading]);\n\n  const count = loaded != null && total != null ? `Showing ${loaded} of ${total}` : null;\n\n  if (mode === 'infinite') {\n    return (\n      <div className='flex min-h-9 flex-col items-center justify-center gap-2 py-4 text-muted-foreground text-sm'>\n        {hasMore ? (\n          <>\n            <div ref={sentinelRef} aria-hidden className='h-px w-full' />\n            {isLoading && <Loader2 className='size-4 animate-spin' />}\n          </>\n        ) : (\n          <span>All caught up</span>\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div className='flex flex-col items-center gap-2 py-4'>\n      {hasMore ? (\n        <Button variant='outline' size='sm' onClick={onLoadMore} disabled={isLoading}>\n          {isLoading && <Loader2 className='mr-2 size-4 animate-spin' />}\n          Load more\n        </Button>\n      ) : (\n        <span className='text-muted-foreground text-sm'>All caught up</span>\n      )}\n      {count && <span className='text-muted-foreground text-xs'>{count}</span>}\n    </div>\n  );\n}\n\ntype FilterField = { id: string; label: string; variant: FilterVariant; options?: DataTableFilterOption[] };\ntype SortField = { id: string; label: string };\n\nfunction getFilterableFields<TData>(table: Table<TData>): FilterField[] {\n  return table\n    .getAllColumns()\n    .filter((column) => column.getCanFilter() && column.columnDef.meta?.variant)\n    .map((column) => ({\n      id: column.id,\n      label: column.columnDef.meta?.label ?? column.id,\n      variant: column.columnDef.meta?.variant as FilterVariant,\n      options: column.columnDef.meta?.options,\n    }));\n}\n\nfunction getSortableFields<TData>(table: Table<TData>): SortField[] {\n  return table\n    .getAllColumns()\n    .filter((column) => column.getCanSort())\n    .map((column) => ({ id: column.id, label: column.columnDef.meta?.label ?? column.id }));\n}\n\nfunction defaultOperatorFor(variant: FilterVariant): FilterOperator {\n  return OPERATORS_BY_VARIANT[variant][0];\n}\n\nfunction defaultValueFor(variant: FilterVariant): unknown {\n  return MULTI_VALUE_OPERATORS.includes(defaultOperatorFor(variant)) ? [] : '';\n}\n\nfunction useReorderSensors() {\n  return useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n  );\n}\n\nfunction SortableRow({ id, children }: { id: string; children: React.ReactNode }) {\n  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });\n  const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition };\n  return (\n    <div ref={setNodeRef} style={style} className={cn('flex items-center gap-2', isDragging && 'opacity-60')}>\n      <button\n        type='button'\n        className='shrink-0 cursor-grab touch-none text-muted-foreground/50 active:cursor-grabbing'\n        aria-label='Reorder'\n        {...attributes}\n        {...listeners}\n      >\n        <GripVertical className='size-4' />\n      </button>\n      {children}\n    </div>\n  );\n}\n\nfunction FieldSelect({\n  fields,\n  value,\n  onChange,\n}: {\n  fields: { id: string; label: string }[];\n  value: string;\n  onChange: (value: string) => void;\n}) {\n  return (\n    <Select value={value} onValueChange={onChange}>\n      <SelectTrigger className='h-8 w-[120px] shrink-0'>\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {fields.map((field) => (\n          <SelectItem key={field.id} value={field.id}>\n            {field.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\nfunction OperatorSelect({\n  variant,\n  value,\n  onChange,\n}: {\n  variant: FilterVariant;\n  value: FilterOperator;\n  onChange: (value: FilterOperator) => void;\n}) {\n  return (\n    <Select value={value} onValueChange={(next) => onChange(next as FilterOperator)}>\n      <SelectTrigger className='h-8 w-[136px] shrink-0'>\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {OPERATORS_BY_VARIANT[variant].map((operator) => (\n          <SelectItem key={operator} value={operator}>\n            {OPERATOR_LABELS[operator]}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\nfunction MultiSelectValue({\n  options,\n  selected,\n  onChange,\n}: {\n  options: DataTableFilterOption[];\n  selected: string[];\n  onChange: (value: string[]) => void;\n}) {\n  const toggle = (value: string) =>\n    onChange(selected.includes(value) ? selected.filter((item) => item !== value) : [...selected, value]);\n  const label =\n    selected.length === 0\n      ? 'Select…'\n      : selected.length === 1\n        ? (options.find((option) => option.value === selected[0])?.label ?? selected[0])\n        : `${selected.length} selected`;\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant='outline' size='sm' className='h-8 flex-1 justify-between font-normal'>\n          <span className='truncate'>{label}</span>\n          <ChevronDown className='ml-1 size-3.5 shrink-0 text-muted-foreground' />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align='start' className='w-48 p-1'>\n        <div className='flex flex-col gap-0.5'>\n          {options.map((option) => (\n            <button\n              key={option.value}\n              type='button'\n              onClick={() => toggle(option.value)}\n              className='flex items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent'\n            >\n              <Checkbox checked={selected.includes(option.value)} className='pointer-events-none' />\n              <span>{option.label}</span>\n            </button>\n          ))}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction FilterValueControl({\n  field,\n  condition,\n  onChange,\n}: {\n  field: FilterField;\n  condition: FilterCondition;\n  onChange: (value: unknown) => void;\n}) {\n  if (!operatorTakesValue(condition.operator)) {\n    return <div className='h-8 flex-1 rounded-md border border-dashed bg-muted/30' aria-hidden />;\n  }\n\n  if ((field.variant === 'select' || field.variant === 'multiSelect') && field.options) {\n    if (MULTI_VALUE_OPERATORS.includes(condition.operator)) {\n      const selected = Array.isArray(condition.value) ? (condition.value as string[]) : [];\n      return <MultiSelectValue options={field.options} selected={selected} onChange={onChange} />;\n    }\n    return (\n      <Select value={(condition.value as string) ?? ''} onValueChange={onChange}>\n        <SelectTrigger className='h-8 flex-1'>\n          <SelectValue placeholder='Select…' />\n        </SelectTrigger>\n        <SelectContent>\n          {field.options.map((option) => (\n            <SelectItem key={option.value} value={option.value}>\n              {option.label}\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n    );\n  }\n\n  return (\n    <Input\n      value={(condition.value as string) ?? ''}\n      onChange={(event) => onChange(event.target.value)}\n      type={field.variant === 'number' ? 'number' : field.variant === 'date' ? 'date' : 'text'}\n      placeholder='Value'\n      className='h-8 flex-1'\n    />\n  );\n}\n\nfunction FilterBuilder<TData>({ table }: { table: Table<TData> }) {\n  const fields = getFilterableFields(table);\n  const columnFilters = table.getState().columnFilters;\n\n  const rows = columnFilters\n    .map((columnFilter) => {\n      const field = fields.find((candidate) => candidate.id === columnFilter.id);\n      if (!field) return null;\n      const condition = isFilterCondition(columnFilter.value)\n        ? columnFilter.value\n        : { operator: defaultOperatorFor(field.variant), value: columnFilter.value };\n      return { field, condition };\n    })\n    .filter((row): row is { field: FilterField; condition: FilterCondition } => row !== null);\n\n  const setCondition = (columnId: string, condition: FilterCondition) => {\n    table.setColumnFilters((current) => [\n      ...current.filter((item) => item.id !== columnId),\n      { id: columnId, value: condition },\n    ]);\n  };\n\n  const removeCondition = (columnId: string) =>\n    table.setColumnFilters((current) => current.filter((item) => item.id !== columnId));\n\n  const changeField = (oldId: string, newId: string) => {\n    const field = fields.find((candidate) => candidate.id === newId);\n    if (!field) return;\n    table.setColumnFilters((current) => [\n      ...current.filter((item) => item.id !== oldId && item.id !== newId),\n      { id: newId, value: { operator: defaultOperatorFor(field.variant), value: defaultValueFor(field.variant) } },\n    ]);\n  };\n\n  const changeOperator = (field: FilterField, previous: FilterCondition, operator: FilterOperator) => {\n    const wasMulti = Array.isArray(previous.value);\n    const willMulti = MULTI_VALUE_OPERATORS.includes(operator);\n    const value = willMulti ? (wasMulti ? previous.value : []) : wasMulti ? '' : previous.value;\n    setCondition(field.id, { operator, value });\n  };\n\n  const addCondition = () => {\n    const used = new Set(columnFilters.map((item) => item.id));\n    const next = fields.find((field) => !used.has(field.id));\n    if (!next) return;\n    setCondition(next.id, { operator: defaultOperatorFor(next.variant), value: defaultValueFor(next.variant) });\n  };\n\n  const allUsed = fields.length > 0 && fields.every((field) => columnFilters.some((item) => item.id === field.id));\n\n  const sensors = useReorderSensors();\n  const handleDragEnd = (event: DragEndEvent) => {\n    const { active, over } = event;\n    if (!over || active.id === over.id) return;\n    table.setColumnFilters((current) => {\n      const oldIndex = current.findIndex((item) => item.id === active.id);\n      const newIndex = current.findIndex((item) => item.id === over.id);\n      if (oldIndex < 0 || newIndex < 0) return current;\n      return arrayMove(current, oldIndex, newIndex);\n    });\n  };\n\n  return (\n    <div className='flex flex-col gap-2'>\n      {rows.length === 0 ? (\n        <p className='px-1 py-2 text-muted-foreground text-sm'>No filters applied to this view.</p>\n      ) : (\n        <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n          <SortableContext items={rows.map((row) => row.field.id)} strategy={verticalListSortingStrategy}>\n            <div className='flex flex-col gap-2'>\n              {rows.map((row, index) => (\n                <SortableRow key={row.field.id} id={row.field.id}>\n                  <span className='w-12 shrink-0 text-muted-foreground text-sm'>{index === 0 ? 'Where' : 'And'}</span>\n                  <FieldSelect\n                    fields={fields}\n                    value={row.field.id}\n                    onChange={(value) => changeField(row.field.id, value)}\n                  />\n                  <OperatorSelect\n                    variant={row.field.variant}\n                    value={row.condition.operator}\n                    onChange={(operator) => changeOperator(row.field, row.condition, operator)}\n                  />\n                  <FilterValueControl\n                    field={row.field}\n                    condition={row.condition}\n                    onChange={(value) => setCondition(row.field.id, { ...row.condition, value })}\n                  />\n                  <Button\n                    variant='ghost'\n                    size='icon-sm'\n                    className='shrink-0 text-muted-foreground'\n                    onClick={() => removeCondition(row.field.id)}\n                    aria-label='Remove filter'\n                  >\n                    <X className='size-4' />\n                  </Button>\n                </SortableRow>\n              ))}\n            </div>\n          </SortableContext>\n        </DndContext>\n      )}\n      <div className='flex items-center justify-between pt-1'>\n        <Button\n          variant='ghost'\n          size='sm'\n          className='h-8 px-2 text-muted-foreground'\n          onClick={addCondition}\n          disabled={allUsed}\n        >\n          <Plus className='mr-1.5 size-4' /> Add filter\n        </Button>\n        {rows.length > 0 && (\n          <Button\n            variant='ghost'\n            size='sm'\n            className='h-8 px-2 text-muted-foreground'\n            onClick={() => table.setColumnFilters([])}\n          >\n            <Trash2 className='mr-1.5 size-4' /> Clear\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport type DataTableFilterMenuProps<TData> = {\n  table: Table<TData>;\n  variant?: 'popover' | 'dialog';\n};\n\nexport function DataTableFilterMenu<TData>({ table, variant = 'popover' }: DataTableFilterMenuProps<TData>) {\n  const count = table.getState().columnFilters.length;\n  const trigger = (\n    <Button variant='outline' size='sm' className='h-8 border-dashed'>\n      <ListFilter className='mr-2 size-4' /> Filter\n      {count > 0 && <span className='ml-2 rounded-sm bg-secondary px-1.5 font-normal text-xs'>{count}</span>}\n      {variant === 'popover' && <ChevronDown className='ml-1 size-3.5 text-muted-foreground' />}\n    </Button>\n  );\n\n  if (variant === 'dialog') {\n    return (\n      <Dialog>\n        <DialogTrigger asChild>{trigger}</DialogTrigger>\n        <DialogContent className='max-w-2xl'>\n          <DialogHeader>\n            <DialogTitle>Filters</DialogTitle>\n            <DialogDescription>Show rows that match all of these conditions.</DialogDescription>\n          </DialogHeader>\n          <FilterBuilder table={table} />\n          <DialogFooter>\n            <Button variant='ghost' size='sm' onClick={() => table.setColumnFilters([])}>\n              Clear all\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    );\n  }\n\n  return (\n    <Popover>\n      <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n      <PopoverContent align='start' className='w-[560px] p-2'>\n        <FilterBuilder table={table} />\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction SortBuilder<TData>({ table }: { table: Table<TData> }) {\n  const fields = getSortableFields(table);\n  const sorting = table.getState().sorting;\n\n  const setDir = (id: string, desc: boolean) =>\n    table.setSorting((current) => current.map((sort) => (sort.id === id ? { ...sort, desc } : sort)));\n\n  const remove = (id: string) => table.setSorting((current) => current.filter((sort) => sort.id !== id));\n\n  const changeField = (oldId: string, newId: string) =>\n    table.setSorting((current) => [\n      ...current.filter((sort) => sort.id !== oldId && sort.id !== newId),\n      { id: newId, desc: false },\n    ]);\n\n  const add = () => {\n    const used = new Set(sorting.map((sort) => sort.id));\n    const next = fields.find((field) => !used.has(field.id));\n    if (next) table.setSorting((current) => [...current, { id: next.id, desc: false }]);\n  };\n\n  const allUsed = fields.length > 0 && fields.every((field) => sorting.some((sort) => sort.id === field.id));\n\n  const sensors = useReorderSensors();\n  const handleDragEnd = (event: DragEndEvent) => {\n    const { active, over } = event;\n    if (!over || active.id === over.id) return;\n    table.setSorting((current) => {\n      const oldIndex = current.findIndex((sort) => sort.id === active.id);\n      const newIndex = current.findIndex((sort) => sort.id === over.id);\n      if (oldIndex < 0 || newIndex < 0) return current;\n      return arrayMove(current, oldIndex, newIndex);\n    });\n  };\n\n  return (\n    <div className='flex flex-col gap-2'>\n      {sorting.length === 0 ? (\n        <p className='px-1 py-2 text-muted-foreground text-sm'>No sorts applied to this view.</p>\n      ) : (\n        <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n          <SortableContext items={sorting.map((sort) => sort.id)} strategy={verticalListSortingStrategy}>\n            <div className='flex flex-col gap-2'>\n              {sorting.map((sort, index) => (\n                <SortableRow key={sort.id} id={sort.id}>\n                  <span className='w-12 shrink-0 text-muted-foreground text-sm'>{index === 0 ? 'Sort' : 'Then'}</span>\n                  <FieldSelect fields={fields} value={sort.id} onChange={(value) => changeField(sort.id, value)} />\n                  <Select\n                    value={sort.desc ? 'desc' : 'asc'}\n                    onValueChange={(value) => setDir(sort.id, value === 'desc')}\n                  >\n                    <SelectTrigger className='h-8 flex-1'>\n                      <SelectValue />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value='asc'>Ascending</SelectItem>\n                      <SelectItem value='desc'>Descending</SelectItem>\n                    </SelectContent>\n                  </Select>\n                  <Button\n                    variant='ghost'\n                    size='icon-sm'\n                    className='shrink-0 text-muted-foreground'\n                    onClick={() => remove(sort.id)}\n                    aria-label='Remove sort'\n                  >\n                    <X className='size-4' />\n                  </Button>\n                </SortableRow>\n              ))}\n            </div>\n          </SortableContext>\n        </DndContext>\n      )}\n      <div className='pt-1'>\n        <Button variant='ghost' size='sm' className='h-8 px-2 text-muted-foreground' onClick={add} disabled={allUsed}>\n          <Plus className='mr-1.5 size-4' /> Add sort\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nexport function DataTableSortMenu<TData>({ table }: { table: Table<TData> }) {\n  const count = table.getState().sorting.length;\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button variant='outline' size='sm' className='h-8 border-dashed'>\n          <ArrowDownUp className='mr-2 size-4' /> Sort\n          {count > 0 && <span className='ml-2 rounded-sm bg-secondary px-1.5 font-normal text-xs'>{count}</span>}\n          <ChevronDown className='ml-1 size-3.5 text-muted-foreground' />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align='start' className='w-[420px] p-2'>\n        <SortBuilder table={table} />\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nexport type DataTableView = { name: string; filters: ColumnFiltersState; sorting: SortingState };\n\nexport type DataTableViewsProps<TData> = {\n  table: Table<TData>;\n  storageKey: string;\n};\n\nexport function DataTableViews<TData>({ table, storageKey }: DataTableViewsProps<TData>) {\n  const key = `shuip:dt-views:${storageKey}`;\n  const [views, setViews] = React.useState<DataTableView[]>([]);\n  const [name, setName] = React.useState('');\n  const [saving, setSaving] = React.useState(false);\n\n  React.useEffect(() => {\n    try {\n      const raw = localStorage.getItem(key);\n      if (raw) setViews(JSON.parse(raw) as DataTableView[]);\n    } catch {\n      setViews([]);\n    }\n  }, [key]);\n\n  const persist = (next: DataTableView[]) => {\n    setViews(next);\n    try {\n      localStorage.setItem(key, JSON.stringify(next));\n    } catch {\n      /* storage unavailable */\n    }\n  };\n\n  const save = () => {\n    const trimmed = name.trim();\n    if (!trimmed) return;\n    const view: DataTableView = {\n      name: trimmed,\n      filters: table.getState().columnFilters,\n      sorting: table.getState().sorting,\n    };\n    persist([...views.filter((current) => current.name !== trimmed), view]);\n    setName('');\n    setSaving(false);\n  };\n\n  const apply = (view: DataTableView) => {\n    table.setColumnFilters(view.filters);\n    table.setSorting(view.sorting);\n  };\n\n  const remove = (viewName: string) => persist(views.filter((current) => current.name !== viewName));\n\n  return (\n    <Popover onOpenChange={(open) => !open && setSaving(false)}>\n      <PopoverTrigger asChild>\n        <Button variant='outline' size='sm' className='h-8'>\n          <Bookmark className='mr-2 size-4' /> Views\n          {views.length > 0 && (\n            <span className='ml-2 rounded-sm bg-secondary px-1.5 font-normal text-xs'>{views.length}</span>\n          )}\n          <ChevronDown className='ml-1 size-3.5 text-muted-foreground' />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent align='end' className='w-60 p-1'>\n        <div className='flex flex-col gap-0.5'>\n          {views.length === 0 ? (\n            <p className='px-2 py-1.5 text-muted-foreground text-sm'>No saved views.</p>\n          ) : (\n            views.map((view) => (\n              <div key={view.name} className='flex items-center'>\n                <button\n                  type='button'\n                  onClick={() => apply(view)}\n                  className='flex-1 truncate rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent'\n                >\n                  {view.name}\n                </button>\n                <Button\n                  variant='ghost'\n                  size='icon-sm'\n                  className='shrink-0 text-muted-foreground'\n                  onClick={() => remove(view.name)}\n                  aria-label={`Delete ${view.name}`}\n                >\n                  <Trash2 className='size-3.5' />\n                </Button>\n              </div>\n            ))\n          )}\n        </div>\n        <Separator className='my-1' />\n        {saving ? (\n          <div className='flex items-center gap-1.5 p-1'>\n            <Input\n              autoFocus\n              value={name}\n              onChange={(event) => setName(event.target.value)}\n              onKeyDown={(event) => event.key === 'Enter' && save()}\n              placeholder='View name'\n              className='h-8'\n            />\n            <Button size='sm' className='h-8' onClick={save} disabled={!name.trim()}>\n              Save\n            </Button>\n          </div>\n        ) : (\n          <Button variant='ghost' size='sm' className='w-full justify-start' onClick={() => setSaving(true)}>\n            <Plus className='mr-2 size-4' /> Save current view\n          </Button>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
