Docs
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
SerpAPI
Netlify
OpenRouter
WorkOS
Clerk
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
Table API Reference
Column API Reference
Row API Reference
Cell API Reference
Header API Reference
Features API Reference
Legacy API Reference
Enterprise

React Example: Kitchen Sink Shadcn Radix

'use client'

import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import '@/styles/globals.css'

import {
  CheckCircle,
  ChevronDown,
  ChevronRight,
  Clock,
  Code,
  CreditCard,
  Megaphone,
  MoreHorizontal,
  Search,
  ShoppingCart,
  Users,
  XCircle,
} from 'lucide-react'
import {
  aggregationFns,
  columnFacetingFeature,
  columnFilteringFeature,
  columnGroupingFeature,
  columnOrderingFeature,
  columnPinningFeature,
  columnResizingFeature,
  columnSizingFeature,
  columnVisibilityFeature,
  createCoreRowModel,
  createExpandedRowModel,
  createFacetedRowModel,
  createFacetedUniqueValues,
  createFilteredRowModel,
  createGroupedRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFns,
  globalFilteringFeature,
  rowExpandingFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFns,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import type { Person } from '@/lib/make-data'
import type {
  CellData,
  Column,
  ColumnDef,
  ColumnPinningState,
  ColumnSizingState,
  ExpandedState,
  GroupingState,
  RowData,
  SortingState,
  TableFeatures,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table'

import { departments, makeData, statuses } from '@/lib/make-data'
import { DataTablePagination } from '@/components/data-table/data-table-pagination'
import { DataTableViewOptions } from '@/components/data-table/data-table-view-options'
import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'

import { Badge } from '@/components/ui/badge'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn, formatDate, toSentenceCase } from '@/lib/utils'
import { DataTableSortList } from '@/components/data-table/data-table-sort-list'
import { DataTableFilterList } from '@/components/data-table/data-table-filter-list'
import { dynamicFilterFn, fuzzyFilter } from '@/lib/data-table'
import { ThemeProvider } from '@/components/theme-provider'
import { ModeToggle } from '@/components/mode-toggle'
import { Input } from '@/components/ui/input'

declare module '@tanstack/react-table' {
  interface ColumnMeta<
    TFeatures extends TableFeatures,
    TData extends RowData,
    TValue extends CellData = CellData,
  > {
    label?: string
    variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select'
    options?: Array<{ label: string; value: string; count?: number }>
  }
}

const _features = tableFeatures({
  rowSortingFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowExpandingFeature,
  columnFilteringFeature,
  columnFacetingFeature,
  columnOrderingFeature,
  columnVisibilityFeature,
  columnSizingFeature,
  columnResizingFeature,
  columnPinningFeature,
  columnGroupingFeature,
  globalFilteringFeature,
})

/**
 * CSS for left/right pinned columns. Verbatim port of the helper from
 * `examples/react/column-pinning-sticky/src/main.tsx` so a pinned column gets
 * `position: sticky` plus the appropriate offset and edge shadow.
 */
function getCommonPinningStyles(
  column: Column<typeof _features, Person>,
  isSelected = false,
): React.CSSProperties {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'left' && column.getIsLastColumn('left')
  const isFirstRightPinnedColumn =
    isPinned === 'right' && column.getIsFirstColumn('right')

  return {
    boxShadow: isLastLeftPinnedColumn
      ? '-4px 0 4px -4px var(--border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--border) inset'
        : undefined,
    left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
    right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
    position: isPinned ? 'sticky' : 'relative',
    background: isSelected
      ? 'var(--muted)'
      : isPinned
        ? 'var(--background)'
        : undefined,
    zIndex: isPinned ? 1 : 0,
  }
}

function App() {
  const rerender = React.useReducer(() => ({}), {})[1]

  const [rowSelection, setRowSelection] = React.useState({})
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<
    Array<ExtendedColumnFilter>
  >([])
  const [columnVisibility, setColumnVisibility] = React.useState({})
  const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
  const [globalFilter, setGlobalFilter] = React.useState('')
  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
    left: ['select'],
    right: ['actions'],
  })
  const [grouping, setGrouping] = React.useState<GroupingState>([])
  const [expanded, setExpanded] = React.useState<ExpandedState>({})

  const columns = React.useMemo<Array<ColumnDef<typeof _features, Person>>>(
    () => [
      {
        id: 'select',
        header: ({ table }) => (
          <Checkbox
            checked={
              table.getIsAllPageRowsSelected() ||
              (table.getIsSomePageRowsSelected() && 'indeterminate')
            }
            onCheckedChange={(value) =>
              table.toggleAllPageRowsSelected(!!value)
            }
            aria-label="Select all"
            className="translate-y-0.5"
          />
        ),
        cell: ({ row }) => (
          <Checkbox
            checked={row.getIsSelected()}
            onCheckedChange={(value) => row.toggleSelected(!!value)}
            aria-label="Select row"
            className="translate-y-0.5"
          />
        ),
        maxSize: 40,
        enableSorting: false,
        enableHiding: false,
        enableResizing: false,
      },
      {
        id: 'firstName',
        accessorKey: 'firstName',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="First Name" />
        ),
        cell: (info) => String(info.getValue()),
        meta: {
          label: 'First Name',
          variant: 'text',
        },
      },
      {
        id: 'lastName',
        accessorFn: (row) => row.lastName,
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Last Name" />
        ),
        cell: (info) => String(info.getValue()),
        meta: {
          label: 'Last Name',
          variant: 'text',
        },
      },
      {
        id: 'age',
        accessorKey: 'age',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Age" />
        ),
        cell: (info) => <span>{String(info.getValue())}</span>,
        aggregationFn: 'mean',
        aggregatedCell: ({ getValue }) => (
          <span className="text-muted-foreground">
            Avg: {Math.round(Number(getValue()) * 10) / 10}
          </span>
        ),
        meta: {
          label: 'Age',
          variant: 'number',
        },
      },
      {
        id: 'email',
        accessorKey: 'email',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Email" />
        ),
        cell: (info) => info.cell.getValue<string>(),
        meta: {
          label: 'Email',
          variant: 'text',
        },
      },
      {
        id: 'status',
        accessorKey: 'status',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Status" />
        ),
        cell: (info) => {
          const status = info.getValue<Person['status'] | undefined>()
          // Group/aggregated rows can pass undefined here — bail out cleanly.
          if (!status) return null
          const icons: Record<Person['status'], React.ReactNode> = {
            active: <CheckCircle />,
            inactive: <XCircle />,
            pending: <Clock />,
          }

          return (
            <Badge
              variant="outline"
              className="gap-1 w-fit [&>svg]:size-3.5 px-3 py-1 [&>svg]:shrink-0 rounded-full"
            >
              {icons[status]}
              <span className="truncate">{toSentenceCase(status)}</span>
            </Badge>
          )
        },
        // Enum column has no useful aggregation; render nothing on group rows.
        aggregatedCell: () => null,
        meta: {
          label: 'Status',
          variant: 'select',
          options: statuses.map((status) => ({
            label: toSentenceCase(status),
            value: status,
          })),
        },
      },
      {
        id: 'department',
        accessorKey: 'department',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Department" />
        ),
        cell: (info) => {
          const department = info.getValue<Person['department'] | undefined>()
          // Group/aggregated rows can pass undefined here — bail out cleanly.
          if (!department) return null
          const icons: Record<Person['department'], React.ReactNode> = {
            engineering: <Code />,
            marketing: <Megaphone />,
            sales: <ShoppingCart />,
            hr: <Users />,
            finance: <CreditCard />,
          }

          return (
            <Badge
              variant="outline"
              className="gap-1 w-fit [&>svg]:size-3.5 px-3 py-1 [&>svg]:shrink-0 rounded-full"
            >
              {icons[department]}
              <span className="truncate">{toSentenceCase(department)}</span>
            </Badge>
          )
        },
        // Enum column has no useful aggregation; render nothing on group rows.
        aggregatedCell: () => null,
        meta: {
          label: 'Department',
          variant: 'multi-select',
          options: departments.map((department) => ({
            label: toSentenceCase(department),
            value: department,
          })),
        },
      },
      {
        id: 'joinDate',
        accessorKey: 'joinDate',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Join Date" />
        ),
        cell: (info) => formatDate(info.getValue<string>()),
        aggregationFn: 'min',
        aggregatedCell: ({ getValue }) => {
          const earliest = getValue<string>()
          return (
            <span className="text-muted-foreground">
              Earliest: {earliest ? formatDate(earliest) : '—'}
            </span>
          )
        },
        meta: {
          label: 'Join Date',
          variant: 'date',
        },
      },
      {
        id: 'actions',
        enableHiding: false,
        cell: ({ row }) => {
          const person = row.original
          return (
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button variant="ghost" className="h-8 w-8 p-0">
                  <span className="sr-only">Open menu</span>
                  <MoreHorizontal className="h-4 w-4" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                <DropdownMenuLabel>Actions</DropdownMenuLabel>
                <DropdownMenuItem
                  onClick={() => navigator.clipboard.writeText(person.id)}
                >
                  Copy ID
                </DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem>View details</DropdownMenuItem>
                <DropdownMenuItem>View profile</DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          )
        },
        maxSize: 30,
        enableResizing: false,
      },
    ],
    [],
  )

  const [data, setData] = React.useState(() => makeData(1_000))
  const [columnOrder, setColumnOrder] = React.useState<Array<string>>(() =>
    columns.map((c) => c.id ?? ''),
  )

  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(100_000))

  const table = useTable(
    {
      _features,
      _rowModels: {
        coreRowModel: createCoreRowModel(),
        filteredRowModel: createFilteredRowModel({
          ...filterFns,
          fuzzy: fuzzyFilter,
        }),
        facetedRowModel: createFacetedRowModel(),
        facetedUniqueValues: createFacetedUniqueValues(),
        paginatedRowModel: createPaginatedRowModel(),
        sortedRowModel: createSortedRowModel(sortFns),
        groupedRowModel: createGroupedRowModel(aggregationFns),
        expandedRowModel: createExpandedRowModel(),
      },
      columns,
      data,
      defaultColumn: {
        minSize: 60,
        maxSize: 800,
        filterFn: dynamicFilterFn,
      },
      globalFilterFn: 'fuzzy',
      state: {
        rowSelection,
        sorting,
        columnVisibility,
        columnOrder,
        columnSizing,
        columnFilters,
        globalFilter,
        columnPinning,
        grouping,
        expanded,
      },
      onSortingChange: setSorting,
      onColumnVisibilityChange: setColumnVisibility,
      onColumnOrderChange: setColumnOrder,
      onColumnSizingChange: setColumnSizing,
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      onColumnPinningChange: setColumnPinning,
      onGroupingChange: setGrouping,
      onExpandedChange: setExpanded,
      getRowId: (row) => row.id,
      enableRowSelection: true,
      onRowSelectionChange: setRowSelection,
      columnResizeMode: 'onChange',
      debugTable: true,
    },
    (state) => state, // subscribe to all re-renders
  )

  const columnSizeVars = React.useMemo(() => {
    const headers = table.getFlatHeaders()
    const colSizes: { [key: string]: number } = {}
    for (const header of headers) {
      colSizes[`--header-${header.id}-size`] = header.getSize()
      colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
    }
    return colSizes
  }, [table.store.state.columnSizing])

  return (
    <div className="container mx-auto p-4 flex flex-col gap-4">
      <div className="flex items-center justify-end gap-2">
        <ModeToggle />
        <Button variant="outline" size="sm" onClick={() => refreshData()}>
          Regenerate Data
        </Button>
        <Button variant="outline" size="sm" onClick={() => stressTest()}>
          Stress Test (100k rows)
        </Button>
        <Button variant="outline" size="sm" onClick={() => rerender()}>
          Force Rerender
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() =>
            console.info(
              'table.getSelectedRowModel().flatRows',
              table.getSelectedRowModel().flatRows,
            )
          }
        >
          Log Selected Rows
        </Button>
      </div>
      <div className="flex flex-col gap-4">
        <div className="flex items-center gap-2">
          <div className="relative w-full max-w-sm">
            <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
            <DebouncedInput
              value={globalFilter}
              onChange={(value) => setGlobalFilter(String(value))}
              placeholder="Search all columns..."
              className="pl-8"
            />
          </div>
          <DataTableFilterList
            table={table}
            columnFilters={columnFilters}
            onColumnFiltersChange={setColumnFilters}
          />
          <DataTableSortList
            table={table}
            sorting={sorting}
            onSortingChange={setSorting}
          />
          <DataTableViewOptions
            table={table}
            columnOrder={columnOrder}
            onColumnOrderChange={setColumnOrder}
          />
        </div>
        <div className="rounded-md border">
          <Table style={{ ...columnSizeVars }}>
            <TableHeader>
              {table.getHeaderGroups().map((headerGroup) => (
                <TableRow key={headerGroup.id}>
                  {headerGroup.headers
                    .filter((header) => header.column.getIsVisible())
                    .map((header) => {
                      return (
                        <TableHead
                          key={header.id}
                          colSpan={header.colSpan}
                          className={cn('relative', {
                            'border-r': header.id !== 'actions',
                            'text-center [&>[role=checkbox]]:mx-auto':
                              header.column.id === 'select',
                          })}
                          style={{
                            width: `calc(var(--header-${header.id}-size) * 1px)`,
                            ...getCommonPinningStyles(header.column),
                          }}
                        >
                          {header.isPlaceholder ? null : (
                            <table.FlexRender header={header} />
                          )}
                          {header.column.getCanResize() && (
                            <div
                              onDoubleClick={() => header.column.resetSize()}
                              onMouseDown={header.getResizeHandler()}
                              onTouchStart={header.getResizeHandler()}
                              className={cn(
                                'absolute right-[-2px] z-10 top-1/2 h-6 w-[3px] -translate-y-1/2 cursor-e-resize select-none touch-none rounded-md transition-colors hover:bg-blue-600 before:absolute before:left-[-4px] before:right-[-4px] before:top-0 before:h-full before:content-[""]',
                                header.column.getIsResizing() && 'bg-blue-600',
                              )}
                            />
                          )}
                        </TableHead>
                      )
                    })}
                </TableRow>
              ))}
            </TableHeader>
            <TableBody>
              {table.getRowModel().rows.map((row) => {
                return (
                  <TableRow
                    key={row.id}
                    data-state={row.getIsSelected() ? 'selected' : undefined}
                    aria-selected={row.getIsSelected()}
                  >
                    {row.getVisibleCells().map((cell) => {
                      return (
                        <TableCell
                          key={cell.id}
                          className={cn(
                            cell.column.id === 'actions' ? '' : 'border-r',
                            cell.column.id === 'select' &&
                              'text-center [&>[role=checkbox]]:mx-auto',
                          )}
                          style={{
                            width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
                            ...getCommonPinningStyles(
                              cell.column,
                              row.getIsSelected(),
                            ),
                          }}
                        >
                          {cell.getIsGrouped() ? (
                            // Group header cell: chevron toggles row expansion,
                            // count shows number of rows in the group.
                            <Button
                              variant="ghost"
                              size="sm"
                              className="-ml-2 h-7 gap-1 px-2"
                              onClick={row.getToggleExpandedHandler()}
                              disabled={!row.getCanExpand()}
                              style={{
                                paddingLeft: `calc(${row.depth} * 1.5rem + 0.5rem)`,
                              }}
                            >
                              {row.getIsExpanded() ? (
                                <ChevronDown className="size-4" />
                              ) : (
                                <ChevronRight className="size-4" />
                              )}
                              <table.FlexRender cell={cell} />
                              <span className="text-muted-foreground">
                                ({row.subRows.length})
                              </span>
                            </Button>
                          ) : (
                            // FlexRender now dispatches based on cell mode:
                            // aggregated → columnDef.aggregatedCell (or
                            // columnDef.cell), placeholder → null, otherwise
                            // columnDef.cell. So we don't need to branch here.
                            <table.FlexRender cell={cell} />
                          )}
                        </TableCell>
                      )
                    })}
                  </TableRow>
                )
              })}
            </TableBody>
          </Table>
        </div>
        <DataTablePagination table={table} />
      </div>
    </div>
  )
}

// Small debounced wrapper around the shadcn Input — adapted from
// `examples/react/filters-fuzzy/src/main.tsx` so the global filter doesn't
// run on every keystroke at 100k rows.
function DebouncedInput({
  value: initialValue,
  onChange,
  debounce = 300,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.ComponentProps<typeof Input>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  React.useEffect(() => {
    const timeout = setTimeout(() => {
      onChange(value)
    }, debounce)

    return () => clearTimeout(timeout)
  }, [value, debounce, onChange])

  return (
    <Input
      {...props}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
      <App />
    </ThemeProvider>
  </React.StrictMode>,
)
'use client'

import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import '@/styles/globals.css'

import {
  CheckCircle,
  ChevronDown,
  ChevronRight,
  Clock,
  Code,
  CreditCard,
  Megaphone,
  MoreHorizontal,
  Search,
  ShoppingCart,
  Users,
  XCircle,
} from 'lucide-react'
import {
  aggregationFns,
  columnFacetingFeature,
  columnFilteringFeature,
  columnGroupingFeature,
  columnOrderingFeature,
  columnPinningFeature,
  columnResizingFeature,
  columnSizingFeature,
  columnVisibilityFeature,
  createCoreRowModel,
  createExpandedRowModel,
  createFacetedRowModel,
  createFacetedUniqueValues,
  createFilteredRowModel,
  createGroupedRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFns,
  globalFilteringFeature,
  rowExpandingFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFns,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import type { Person } from '@/lib/make-data'
import type {
  CellData,
  Column,
  ColumnDef,
  ColumnPinningState,
  ColumnSizingState,
  ExpandedState,
  GroupingState,
  RowData,
  SortingState,
  TableFeatures,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table'

import { departments, makeData, statuses } from '@/lib/make-data'
import { DataTablePagination } from '@/components/data-table/data-table-pagination'
import { DataTableViewOptions } from '@/components/data-table/data-table-view-options'
import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'

import { Badge } from '@/components/ui/badge'
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn, formatDate, toSentenceCase } from '@/lib/utils'
import { DataTableSortList } from '@/components/data-table/data-table-sort-list'
import { DataTableFilterList } from '@/components/data-table/data-table-filter-list'
import { dynamicFilterFn, fuzzyFilter } from '@/lib/data-table'
import { ThemeProvider } from '@/components/theme-provider'
import { ModeToggle } from '@/components/mode-toggle'
import { Input } from '@/components/ui/input'

declare module '@tanstack/react-table' {
  interface ColumnMeta<
    TFeatures extends TableFeatures,
    TData extends RowData,
    TValue extends CellData = CellData,
  > {
    label?: string
    variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select'
    options?: Array<{ label: string; value: string; count?: number }>
  }
}

const _features = tableFeatures({
  rowSortingFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowExpandingFeature,
  columnFilteringFeature,
  columnFacetingFeature,
  columnOrderingFeature,
  columnVisibilityFeature,
  columnSizingFeature,
  columnResizingFeature,
  columnPinningFeature,
  columnGroupingFeature,
  globalFilteringFeature,
})

/**
 * CSS for left/right pinned columns. Verbatim port of the helper from
 * `examples/react/column-pinning-sticky/src/main.tsx` so a pinned column gets
 * `position: sticky` plus the appropriate offset and edge shadow.
 */
function getCommonPinningStyles(
  column: Column<typeof _features, Person>,
  isSelected = false,
): React.CSSProperties {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'left' && column.getIsLastColumn('left')
  const isFirstRightPinnedColumn =
    isPinned === 'right' && column.getIsFirstColumn('right')

  return {
    boxShadow: isLastLeftPinnedColumn
      ? '-4px 0 4px -4px var(--border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--border) inset'
        : undefined,
    left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
    right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
    position: isPinned ? 'sticky' : 'relative',
    background: isSelected
      ? 'var(--muted)'
      : isPinned
        ? 'var(--background)'
        : undefined,
    zIndex: isPinned ? 1 : 0,
  }
}

function App() {
  const rerender = React.useReducer(() => ({}), {})[1]

  const [rowSelection, setRowSelection] = React.useState({})
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<
    Array<ExtendedColumnFilter>
  >([])
  const [columnVisibility, setColumnVisibility] = React.useState({})
  const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
  const [globalFilter, setGlobalFilter] = React.useState('')
  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
    left: ['select'],
    right: ['actions'],
  })
  const [grouping, setGrouping] = React.useState<GroupingState>([])
  const [expanded, setExpanded] = React.useState<ExpandedState>({})

  const columns = React.useMemo<Array<ColumnDef<typeof _features, Person>>>(
    () => [
      {
        id: 'select',
        header: ({ table }) => (
          <Checkbox
            checked={
              table.getIsAllPageRowsSelected() ||
              (table.getIsSomePageRowsSelected() && 'indeterminate')
            }
            onCheckedChange={(value) =>
              table.toggleAllPageRowsSelected(!!value)
            }
            aria-label="Select all"
            className="translate-y-0.5"
          />
        ),
        cell: ({ row }) => (
          <Checkbox
            checked={row.getIsSelected()}
            onCheckedChange={(value) => row.toggleSelected(!!value)}
            aria-label="Select row"
            className="translate-y-0.5"
          />
        ),
        maxSize: 40,
        enableSorting: false,
        enableHiding: false,
        enableResizing: false,
      },
      {
        id: 'firstName',
        accessorKey: 'firstName',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="First Name" />
        ),
        cell: (info) => String(info.getValue()),
        meta: {
          label: 'First Name',
          variant: 'text',
        },
      },
      {
        id: 'lastName',
        accessorFn: (row) => row.lastName,
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Last Name" />
        ),
        cell: (info) => String(info.getValue()),
        meta: {
          label: 'Last Name',
          variant: 'text',
        },
      },
      {
        id: 'age',
        accessorKey: 'age',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Age" />
        ),
        cell: (info) => <span>{String(info.getValue())}</span>,
        aggregationFn: 'mean',
        aggregatedCell: ({ getValue }) => (
          <span className="text-muted-foreground">
            Avg: {Math.round(Number(getValue()) * 10) / 10}
          </span>
        ),
        meta: {
          label: 'Age',
          variant: 'number',
        },
      },
      {
        id: 'email',
        accessorKey: 'email',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Email" />
        ),
        cell: (info) => info.cell.getValue<string>(),
        meta: {
          label: 'Email',
          variant: 'text',
        },
      },
      {
        id: 'status',
        accessorKey: 'status',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Status" />
        ),
        cell: (info) => {
          const status = info.getValue<Person['status'] | undefined>()
          // Group/aggregated rows can pass undefined here — bail out cleanly.
          if (!status) return null
          const icons: Record<Person['status'], React.ReactNode> = {
            active: <CheckCircle />,
            inactive: <XCircle />,
            pending: <Clock />,
          }

          return (
            <Badge
              variant="outline"
              className="gap-1 w-fit [&>svg]:size-3.5 px-3 py-1 [&>svg]:shrink-0 rounded-full"
            >
              {icons[status]}
              <span className="truncate">{toSentenceCase(status)}</span>
            </Badge>
          )
        },
        // Enum column has no useful aggregation; render nothing on group rows.
        aggregatedCell: () => null,
        meta: {
          label: 'Status',
          variant: 'select',
          options: statuses.map((status) => ({
            label: toSentenceCase(status),
            value: status,
          })),
        },
      },
      {
        id: 'department',
        accessorKey: 'department',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Department" />
        ),
        cell: (info) => {
          const department = info.getValue<Person['department'] | undefined>()
          // Group/aggregated rows can pass undefined here — bail out cleanly.
          if (!department) return null
          const icons: Record<Person['department'], React.ReactNode> = {
            engineering: <Code />,
            marketing: <Megaphone />,
            sales: <ShoppingCart />,
            hr: <Users />,
            finance: <CreditCard />,
          }

          return (
            <Badge
              variant="outline"
              className="gap-1 w-fit [&>svg]:size-3.5 px-3 py-1 [&>svg]:shrink-0 rounded-full"
            >
              {icons[department]}
              <span className="truncate">{toSentenceCase(department)}</span>
            </Badge>
          )
        },
        // Enum column has no useful aggregation; render nothing on group rows.
        aggregatedCell: () => null,
        meta: {
          label: 'Department',
          variant: 'multi-select',
          options: departments.map((department) => ({
            label: toSentenceCase(department),
            value: department,
          })),
        },
      },
      {
        id: 'joinDate',
        accessorKey: 'joinDate',
        header: ({ column }) => (
          <DataTableColumnHeader column={column} title="Join Date" />
        ),
        cell: (info) => formatDate(info.getValue<string>()),
        aggregationFn: 'min',
        aggregatedCell: ({ getValue }) => {
          const earliest = getValue<string>()
          return (
            <span className="text-muted-foreground">
              Earliest: {earliest ? formatDate(earliest) : ''}
            </span>
          )
        },
        meta: {
          label: 'Join Date',
          variant: 'date',
        },
      },
      {
        id: 'actions',
        enableHiding: false,
        cell: ({ row }) => {
          const person = row.original
          return (
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button variant="ghost" className="h-8 w-8 p-0">
                  <span className="sr-only">Open menu</span>
                  <MoreHorizontal className="h-4 w-4" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                <DropdownMenuLabel>Actions</DropdownMenuLabel>
                <DropdownMenuItem
                  onClick={() => navigator.clipboard.writeText(person.id)}
                >
                  Copy ID
                </DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem>View details</DropdownMenuItem>
                <DropdownMenuItem>View profile</DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          )
        },
        maxSize: 30,
        enableResizing: false,
      },
    ],
    [],
  )

  const [data, setData] = React.useState(() => makeData(1_000))
  const [columnOrder, setColumnOrder] = React.useState<Array<string>>(() =>
    columns.map((c) => c.id ?? ''),
  )

  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(100_000))

  const table = useTable(
    {
      _features,
      _rowModels: {
        coreRowModel: createCoreRowModel(),
        filteredRowModel: createFilteredRowModel({
          ...filterFns,
          fuzzy: fuzzyFilter,
        }),
        facetedRowModel: createFacetedRowModel(),
        facetedUniqueValues: createFacetedUniqueValues(),
        paginatedRowModel: createPaginatedRowModel(),
        sortedRowModel: createSortedRowModel(sortFns),
        groupedRowModel: createGroupedRowModel(aggregationFns),
        expandedRowModel: createExpandedRowModel(),
      },
      columns,
      data,
      defaultColumn: {
        minSize: 60,
        maxSize: 800,
        filterFn: dynamicFilterFn,
      },
      globalFilterFn: 'fuzzy',
      state: {
        rowSelection,
        sorting,
        columnVisibility,
        columnOrder,
        columnSizing,
        columnFilters,
        globalFilter,
        columnPinning,
        grouping,
        expanded,
      },
      onSortingChange: setSorting,
      onColumnVisibilityChange: setColumnVisibility,
      onColumnOrderChange: setColumnOrder,
      onColumnSizingChange: setColumnSizing,
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      onColumnPinningChange: setColumnPinning,
      onGroupingChange: setGrouping,
      onExpandedChange: setExpanded,
      getRowId: (row) => row.id,
      enableRowSelection: true,
      onRowSelectionChange: setRowSelection,
      columnResizeMode: 'onChange',
      debugTable: true,
    },
    (state) => state, // subscribe to all re-renders
  )

  const columnSizeVars = React.useMemo(() => {
    const headers = table.getFlatHeaders()
    const colSizes: { [key: string]: number } = {}
    for (const header of headers) {
      colSizes[`--header-${header.id}-size`] = header.getSize()
      colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
    }
    return colSizes
  }, [table.store.state.columnSizing])

  return (
    <div className="container mx-auto p-4 flex flex-col gap-4">
      <div className="flex items-center justify-end gap-2">
        <ModeToggle />
        <Button variant="outline" size="sm" onClick={() => refreshData()}>
          Regenerate Data
        </Button>
        <Button variant="outline" size="sm" onClick={() => stressTest()}>
          Stress Test (100k rows)
        </Button>
        <Button variant="outline" size="sm" onClick={() => rerender()}>
          Force Rerender
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() =>
            console.info(
              'table.getSelectedRowModel().flatRows',
              table.getSelectedRowModel().flatRows,
            )
          }
        >
          Log Selected Rows
        </Button>
      </div>
      <div className="flex flex-col gap-4">
        <div className="flex items-center gap-2">
          <div className="relative w-full max-w-sm">
            <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
            <DebouncedInput
              value={globalFilter}
              onChange={(value) => setGlobalFilter(String(value))}
              placeholder="Search all columns..."
              className="pl-8"
            />
          </div>
          <DataTableFilterList
            table={table}
            columnFilters={columnFilters}
            onColumnFiltersChange={setColumnFilters}
          />
          <DataTableSortList
            table={table}
            sorting={sorting}
            onSortingChange={setSorting}
          />
          <DataTableViewOptions
            table={table}
            columnOrder={columnOrder}
            onColumnOrderChange={setColumnOrder}
          />
        </div>
        <div className="rounded-md border">
          <Table style={{ ...columnSizeVars }}>
            <TableHeader>
              {table.getHeaderGroups().map((headerGroup) => (
                <TableRow key={headerGroup.id}>
                  {headerGroup.headers
                    .filter((header) => header.column.getIsVisible())
                    .map((header) => {
                      return (
                        <TableHead
                          key={header.id}
                          colSpan={header.colSpan}
                          className={cn('relative', {
                            'border-r': header.id !== 'actions',
                            'text-center [&>[role=checkbox]]:mx-auto':
                              header.column.id === 'select',
                          })}
                          style={{
                            width: `calc(var(--header-${header.id}-size) * 1px)`,
                            ...getCommonPinningStyles(header.column),
                          }}
                        >
                          {header.isPlaceholder ? null : (
                            <table.FlexRender header={header} />
                          )}
                          {header.column.getCanResize() && (
                            <div
                              onDoubleClick={() => header.column.resetSize()}
                              onMouseDown={header.getResizeHandler()}
                              onTouchStart={header.getResizeHandler()}
                              className={cn(
                                'absolute right-[-2px] z-10 top-1/2 h-6 w-[3px] -translate-y-1/2 cursor-e-resize select-none touch-none rounded-md transition-colors hover:bg-blue-600 before:absolute before:left-[-4px] before:right-[-4px] before:top-0 before:h-full before:content-[""]',
                                header.column.getIsResizing() && 'bg-blue-600',
                              )}
                            />
                          )}
                        </TableHead>
                      )
                    })}
                </TableRow>
              ))}
            </TableHeader>
            <TableBody>
              {table.getRowModel().rows.map((row) => {
                return (
                  <TableRow
                    key={row.id}
                    data-state={row.getIsSelected() ? 'selected' : undefined}
                    aria-selected={row.getIsSelected()}
                  >
                    {row.getVisibleCells().map((cell) => {
                      return (
                        <TableCell
                          key={cell.id}
                          className={cn(
                            cell.column.id === 'actions' ? '' : 'border-r',
                            cell.column.id === 'select' &&
                              'text-center [&>[role=checkbox]]:mx-auto',
                          )}
                          style={{
                            width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
                            ...getCommonPinningStyles(
                              cell.column,
                              row.getIsSelected(),
                            ),
                          }}
                        >
                          {cell.getIsGrouped() ? (
                            // Group header cell: chevron toggles row expansion,
                            // count shows number of rows in the group.
                            <Button
                              variant="ghost"
                              size="sm"
                              className="-ml-2 h-7 gap-1 px-2"
                              onClick={row.getToggleExpandedHandler()}
                              disabled={!row.getCanExpand()}
                              style={{
                                paddingLeft: `calc(${row.depth} * 1.5rem + 0.5rem)`,
                              }}
                            >
                              {row.getIsExpanded() ? (
                                <ChevronDown className="size-4" />
                              ) : (
                                <ChevronRight className="size-4" />
                              )}
                              <table.FlexRender cell={cell} />
                              <span className="text-muted-foreground">
                                ({row.subRows.length})
                              </span>
                            </Button>
                          ) : (
                            // FlexRender now dispatches based on cell mode:
                            // aggregated → columnDef.aggregatedCell (or
                            // columnDef.cell), placeholder → null, otherwise
                            // columnDef.cell. So we don't need to branch here.
                            <table.FlexRender cell={cell} />
                          )}
                        </TableCell>
                      )
                    })}
                  </TableRow>
                )
              })}
            </TableBody>
          </Table>
        </div>
        <DataTablePagination table={table} />
      </div>
    </div>
  )
}

// Small debounced wrapper around the shadcn Input — adapted from
// `examples/react/filters-fuzzy/src/main.tsx` so the global filter doesn't
// run on every keystroke at 100k rows.
function DebouncedInput({
  value: initialValue,
  onChange,
  debounce = 300,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.ComponentProps<typeof Input>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  React.useEffect(() => {
    const timeout = setTimeout(() => {
      onChange(value)
    }, debounce)

    return () => clearTimeout(timeout)
  }, [value, debounce, onChange])

  return (
    <Input
      {...props}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
      <App />
    </ThemeProvider>
  </React.StrictMode>,
)