{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-20",
  "title": "Complete data table",
  "description": "Filters, column toggles, row actions, bulk delete and pagination together.",
  "dependencies": [
    "@tanstack/react-table",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/alert-dialog.json",
    "https://ui.ndekocode.dev/r/badge.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/checkbox.json",
    "https://ui.ndekocode.dev/r/dropdown-menu.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/input.json",
    "https://ui.ndekocode.dev/r/kbd.json",
    "https://ui.ndekocode.dev/r/label.json",
    "https://ui.ndekocode.dev/r/motion-number.json",
    "https://ui.ndekocode.dev/r/pagination.json",
    "https://ui.ndekocode.dev/r/popover.json",
    "https://ui.ndekocode.dev/r/select.json",
    "https://ui.ndekocode.dev/r/table.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/table/table-20.tsx",
      "content": "'use client';\n\nimport {\n  type Column,\n  type ColumnDef,\n  type ColumnFiltersState,\n  type FilterFn,\n  flexRender,\n  getCoreRowModel,\n  getFacetedUniqueValues,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  type Header,\n  type PaginationState,\n  type Row,\n  type SortDirection,\n  type SortingState,\n  useReactTable,\n  type VisibilityState,\n} from '@tanstack/react-table';\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { createContext, useContext, useId, useRef, useState } from 'react';\n\nimport { MotionNumber } from '@/components/core/motion-number';\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport {\n  ChevronDownIcon,\n  ChevronFirstIcon,\n  ChevronLastIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronsUpDownIcon,\n  ChevronUpIcon,\n  CircleAlertIcon,\n  CircleXIcon,\n  Columns3Icon,\n  EllipsisIcon,\n  FilterIcon,\n  ListFilterIcon,\n  PlusIcon,\n  TrashIcon,\n} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from '@/registry/ndeko/ui/alert-dialog';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Checkbox } from '@/registry/ndeko/ui/checkbox';\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from '@/registry/ndeko/ui/dropdown-menu';\nimport { Input } from '@/registry/ndeko/ui/input';\nimport { Kbd, KbdGroup } from '@/registry/ndeko/ui/kbd';\nimport { Label } from '@/registry/ndeko/ui/label';\nimport {\n  Pagination,\n  PaginationContent,\n  PaginationItem,\n} from '@/registry/ndeko/ui/pagination';\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/registry/ndeko/ui/popover';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/registry/ndeko/ui/select';\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/registry/ndeko/ui/table';\n\ntype Status = 'Active' | 'Inactive' | 'Pending';\n\ntype Account = {\n  id: string;\n  name: string;\n  email: string;\n  location: string;\n  status: Status;\n  balance: number;\n};\n\nconst STATUS_VARIANTS = {\n  Active: 'success',\n  Inactive: 'secondary',\n  Pending: 'warning',\n} as const;\n\nconst ACCOUNTS: Account[] = [\n  {\n    id: '1',\n    name: 'Jordan Hayes',\n    email: 'jordan.hayes@example.com',\n    location: 'Toronto, Canada',\n    status: 'Active',\n    balance: 1250,\n  },\n  {\n    id: '2',\n    name: 'Anna Novak',\n    email: 'anna.novak@example.com',\n    location: 'Prague, Czechia',\n    status: 'Active',\n    balance: 600,\n  },\n  {\n    id: '3',\n    name: 'Kenji Watanabe',\n    email: 'kenji.watanabe@example.com',\n    location: 'Osaka, Japan',\n    status: 'Inactive',\n    balance: 650,\n  },\n  {\n    id: '4',\n    name: 'Lina Haddad',\n    email: 'lina.haddad@example.com',\n    location: 'Lyon, France',\n    status: 'Active',\n    balance: 0,\n  },\n  {\n    id: '5',\n    name: 'Ama Mensah',\n    email: 'ama.mensah@example.com',\n    location: 'Accra, Ghana',\n    status: 'Pending',\n    balance: -1000,\n  },\n  {\n    id: '6',\n    name: 'Mateo Rossi',\n    email: 'mateo.rossi@example.com',\n    location: 'Turin, Italy',\n    status: 'Active',\n    balance: 1500,\n  },\n  {\n    id: '7',\n    name: 'Priya Raman',\n    email: 'priya.raman@example.com',\n    location: 'Bengaluru, India',\n    status: 'Inactive',\n    balance: 200,\n  },\n  {\n    id: '8',\n    name: 'Lucas Meyer',\n    email: 'lucas.meyer@example.com',\n    location: 'Hamburg, Germany',\n    status: 'Active',\n    balance: 1000,\n  },\n  {\n    id: '9',\n    name: 'Sofia Lindqvist',\n    email: 'sofia.lindqvist@example.com',\n    location: 'Malmö, Sweden',\n    status: 'Active',\n    balance: 500,\n  },\n  {\n    id: '10',\n    name: 'Daniel Okafor',\n    email: 'daniel.okafor@example.com',\n    location: 'Lagos, Nigeria',\n    status: 'Inactive',\n    balance: 300,\n  },\n  {\n    id: '11',\n    name: 'Mei Lin',\n    email: 'mei.lin@example.com',\n    location: 'Taipei, Taiwan',\n    status: 'Active',\n    balance: 800,\n  },\n  {\n    id: '12',\n    name: 'Omar Farouk',\n    email: 'omar.farouk@example.com',\n    location: 'Cairo, Egypt',\n    status: 'Active',\n    balance: 1200,\n  },\n  {\n    id: '13',\n    name: 'Clara Duarte',\n    email: 'clara.duarte@example.com',\n    location: 'Porto, Portugal',\n    status: 'Pending',\n    balance: 400,\n  },\n  {\n    id: '14',\n    name: 'Noah Becker',\n    email: 'noah.becker@example.com',\n    location: 'Zurich, Switzerland',\n    status: 'Inactive',\n    balance: 600,\n  },\n  {\n    id: '15',\n    name: 'Hana Kim',\n    email: 'hana.kim@example.com',\n    location: 'Busan, South Korea',\n    status: 'Active',\n    balance: 1800,\n  },\n  {\n    id: '16',\n    name: 'Ethan Brooks',\n    email: 'ethan.brooks@example.com',\n    location: 'Denver, United States',\n    status: 'Active',\n    balance: 950,\n  },\n  {\n    id: '17',\n    name: 'Yara Nasser',\n    email: 'yara.nasser@example.com',\n    location: 'Amman, Jordan',\n    status: 'Pending',\n    balance: 150,\n  },\n  {\n    id: '18',\n    name: 'Felix Wagner',\n    email: 'felix.wagner@example.com',\n    location: 'Vienna, Austria',\n    status: 'Active',\n    balance: 2100,\n  },\n  {\n    id: '19',\n    name: 'Aiko Tanaka',\n    email: 'aiko.tanaka@example.com',\n    location: 'Kyoto, Japan',\n    status: 'Inactive',\n    balance: 0,\n  },\n  {\n    id: '20',\n    name: 'Samuel Mukendi',\n    email: 'samuel.mukendi@example.com',\n    location: 'Kinshasa, DR Congo',\n    status: 'Active',\n    balance: 700,\n  },\n];\n\nconst CURRENCY = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n});\n\nconst FIGURE_COLUMNS = new Set(['balance']);\n\nconst PAGE_SIZES = [\n  { value: 5, label: '5' },\n  { value: 10, label: '10' },\n  { value: 25, label: '25' },\n  { value: 50, label: '50' },\n];\n\nconst INITIAL_PAGINATION: PaginationState = { pageIndex: 0, pageSize: 10 };\n\nconst SWAP_TRANSITION: Transition = { duration: 0.15, ease: [0.2, 0, 0, 1] };\n\nconst GLYPH_VARIANTS: Variants = {\n  enter: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n  visible: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n  exit: { opacity: 0, scale: 0.6, filter: 'blur(2px)' },\n};\n\nconst FADE_VARIANTS: Variants = {\n  enter: { opacity: 0 },\n  visible: { opacity: 1 },\n  exit: { opacity: 0 },\n};\n\nconst STATUSES: Status[] = ['Active', 'Inactive', 'Pending'];\n\nconst COUNT_BADGE =\n  'keycap-3d text-muted-foreground -me-1 inline-flex h-5 min-w-5 items-center justify-center overflow-hidden rounded-[5px] px-1 text-[11px] font-medium tabular-nums';\n\nconst PRESENCE_VARIANTS: Variants = {\n  hidden: {\n    opacity: 0,\n    scale: 0.9,\n    filter: 'blur(2px)',\n    transition: { duration: 0.12, ease: [0.23, 1, 0.32, 1] },\n  },\n  shown: {\n    opacity: 1,\n    scale: 1,\n    filter: 'blur(0px)',\n    transition: { duration: 0.18, ease: [0.23, 1, 0.32, 1] },\n  },\n};\n\nconst PRESENCE_FADE_VARIANTS: Variants = {\n  hidden: { opacity: 0, transition: { duration: 0.12 } },\n  shown: { opacity: 1, transition: { duration: 0.18 } },\n};\n\nconst DeleteAccountContext = createContext<(id: string) => void>(() => {});\n\nconst matchesNameOrEmail: FilterFn<Account> = (row, _columnId, filterValue) => {\n  const content = `${row.original.name} ${row.original.email}`;\n  const searchableContent = content.toLowerCase();\n  const searchTerm = String(filterValue ?? '').toLowerCase();\n  return searchableContent.includes(searchTerm);\n};\n\nconst matchesStatuses: FilterFn<Account> = (row, columnId, filterValue) => {\n  const statuses: string[] = filterValue ?? [];\n  if (statuses.length === 0) {\n    return true;\n  }\n  const status = row.getValue<string>(columnId);\n  return statuses.includes(status);\n};\n\nfunction Shortcut({ keys }: { keys: string[] }) {\n  return (\n    <DropdownMenuShortcut>\n      <KbdGroup>\n        {keys.map((key) => (\n          <Kbd key={key}>{key}</Kbd>\n        ))}\n      </KbdGroup>\n    </DropdownMenuShortcut>\n  );\n}\n\nfunction RowActions({ row }: { row: Row<Account> }) {\n  const deleteAccount = useContext(DeleteAccountContext);\n\n  return (\n    <div className='flex justify-end'>\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          render={\n            <Button\n              variant='ghost'\n              size='icon-sm'\n              aria-label={`Actions for ${row.original.name}`}\n              className='text-muted-foreground hover:text-foreground'\n            />\n          }\n        >\n          <EllipsisIcon />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align='end'>\n          <DropdownMenuGroup>\n            <DropdownMenuItem>\n              Edit\n              <Shortcut keys={['⌘', 'E']} />\n            </DropdownMenuItem>\n            <DropdownMenuItem>\n              Duplicate\n              <Shortcut keys={['⌘', 'D']} />\n            </DropdownMenuItem>\n          </DropdownMenuGroup>\n          <DropdownMenuSeparator />\n          <DropdownMenuGroup>\n            <DropdownMenuItem>\n              Archive\n              <Shortcut keys={['⌘', 'A']} />\n            </DropdownMenuItem>\n            <DropdownMenuSub>\n              <DropdownMenuSubTrigger>More</DropdownMenuSubTrigger>\n              <DropdownMenuSubContent>\n                <DropdownMenuItem>Move to project</DropdownMenuItem>\n                <DropdownMenuItem>Move to folder</DropdownMenuItem>\n                <DropdownMenuSeparator />\n                <DropdownMenuItem>Advanced options</DropdownMenuItem>\n              </DropdownMenuSubContent>\n            </DropdownMenuSub>\n          </DropdownMenuGroup>\n          <DropdownMenuSeparator />\n          <DropdownMenuGroup>\n            <DropdownMenuItem>Share</DropdownMenuItem>\n            <DropdownMenuItem>Add to favorites</DropdownMenuItem>\n          </DropdownMenuGroup>\n          <DropdownMenuSeparator />\n          <DropdownMenuItem\n            variant='destructive'\n            onClick={() => {\n              deleteAccount(row.original.id);\n            }}\n          >\n            Delete\n            <Shortcut keys={['⌘', '⌫']} />\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  );\n}\n\nconst COLUMNS: ColumnDef<Account>[] = [\n  {\n    id: 'select',\n    header: ({ table }) => (\n      <Checkbox\n        checked={table.getIsAllPageRowsSelected()}\n        indeterminate={table.getIsSomePageRowsSelected()}\n        onCheckedChange={(checked) => {\n          table.toggleAllPageRowsSelected(checked);\n        }}\n        aria-label='Select all rows on this page'\n      />\n    ),\n    cell: ({ row }) => (\n      <Checkbox\n        checked={row.getIsSelected()}\n        onCheckedChange={(checked) => {\n          row.toggleSelected(checked);\n        }}\n        aria-label={`Select ${row.original.name}`}\n      />\n    ),\n    size: 28,\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    header: 'Name',\n    accessorKey: 'name',\n    cell: ({ row }) => <span className='font-medium'>{row.original.name}</span>,\n    size: 180,\n    filterFn: matchesNameOrEmail,\n    enableHiding: false,\n  },\n  {\n    header: 'Email',\n    accessorKey: 'email',\n    cell: ({ row }) => (\n      <span className='text-muted-foreground'>{row.original.email}</span>\n    ),\n    size: 220,\n  },\n  {\n    header: 'Location',\n    accessorKey: 'location',\n    size: 180,\n  },\n  {\n    header: 'Status',\n    accessorKey: 'status',\n    cell: ({ row }) => (\n      <Badge variant={STATUS_VARIANTS[row.original.status]}>\n        {row.original.status}\n      </Badge>\n    ),\n    size: 110,\n    filterFn: matchesStatuses,\n  },\n  {\n    header: 'Balance',\n    accessorKey: 'balance',\n    cell: ({ row }) => CURRENCY.format(row.original.balance),\n    size: 120,\n  },\n  {\n    id: 'actions',\n    header: () => <span className='sr-only'>Actions</span>,\n    cell: ({ row }) => <RowActions row={row} />,\n    size: 56,\n    enableSorting: false,\n    enableHiding: false,\n  },\n];\n\nfunction isFigureColumn(columnId: string) {\n  return FIGURE_COLUMNS.has(columnId);\n}\n\nfunction alignClassOf(columnId: string) {\n  if (isFigureColumn(columnId)) {\n    return 'text-right tabular-nums';\n  }\n  return 'truncate';\n}\n\nfunction sortButtonOrderOf(columnId: string) {\n  if (isFigureColumn(columnId)) {\n    return 'flex-row-reverse';\n  }\n  return undefined;\n}\n\nfunction rowStateOf(isSelected: boolean) {\n  if (isSelected) {\n    return 'selected';\n  }\n  return undefined;\n}\n\nfunction ariaSortOf(column: Column<Account>) {\n  if (!column.getCanSort()) {\n    return undefined;\n  }\n  const direction = column.getIsSorted();\n  if (direction === 'asc') {\n    return 'ascending';\n  }\n  if (direction === 'desc') {\n    return 'descending';\n  }\n  return 'none';\n}\n\nfunction glyphVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return FADE_VARIANTS;\n  }\n  return GLYPH_VARIANTS;\n}\n\nfunction sortGlyphOf(direction: false | SortDirection) {\n  if (direction === 'asc') {\n    return {\n      key: 'asc',\n      icon: <ChevronUpIcon className='text-foreground size-3.5' />,\n    };\n  }\n  if (direction === 'desc') {\n    return {\n      key: 'desc',\n      icon: <ChevronDownIcon className='text-foreground size-3.5' />,\n    };\n  }\n  return {\n    key: 'none',\n    icon: <ChevronsUpDownIcon className='size-3.5 opacity-40' />,\n  };\n}\n\nfunction rangeOf(pagination: PaginationState, rowCount: number) {\n  const rowsBefore = pagination.pageIndex * pagination.pageSize;\n  const firstOfPage = rowsBefore + 1;\n  const firstRow = Math.min(firstOfPage, rowCount);\n  const lastOfPage = rowsBefore + pagination.pageSize;\n  const lastRow = Math.min(lastOfPage, rowCount);\n  return { firstRow, lastRow };\n}\n\nfunction SortGlyph({ direction }: { direction: false | SortDirection }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const variants = glyphVariantsOf(shouldReduceMotion);\n  const glyph = sortGlyphOf(direction);\n\n  return (\n    <span aria-hidden='true' className='relative inline-flex size-3.5'>\n      <AnimatePresence mode='popLayout' initial={false}>\n        <motion.span\n          key={glyph.key}\n          className='inline-flex'\n          variants={variants}\n          initial='enter'\n          animate='visible'\n          exit='exit'\n          transition={SWAP_TRANSITION}\n        >\n          {glyph.icon}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction HeadContent({ header }: { header: Header<Account, unknown> }) {\n  if (header.isPlaceholder) {\n    return null;\n  }\n  const label = flexRender(header.column.columnDef.header, header.getContext());\n  if (!header.column.getCanSort()) {\n    return label;\n  }\n  return (\n    <Button\n      variant='ghost'\n      size='sm'\n      onClick={header.column.getToggleSortingHandler()}\n      className={cn(\n        'text-muted-foreground hover:text-foreground -mx-2.5 gap-1.5',\n        sortButtonOrderOf(header.column.id)\n      )}\n    >\n      {label}\n      <SortGlyph direction={header.column.getIsSorted()} />\n    </Button>\n  );\n}\n\nfunction labelOf(column: Column<Account>) {\n  const header = column.columnDef.header;\n  if (typeof header === 'string') {\n    return header;\n  }\n  return column.id;\n}\n\nfunction statusesOf(value: unknown): string[] {\n  if (Array.isArray(value)) {\n    return value;\n  }\n  return [];\n}\n\nfunction accountCountLabel(count: number) {\n  if (count === 1) {\n    return '1 account';\n  }\n  return `${count} accounts`;\n}\n\nfunction presenceVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return PRESENCE_FADE_VARIANTS;\n  }\n  return PRESENCE_VARIANTS;\n}\n\nfunction NameFilter({ column }: { column: Column<Account> | undefined }) {\n  const id = useId();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const variants = presenceVariantsOf(shouldReduceMotion);\n  if (!column) {\n    return null;\n  }\n  const value = String(column.getFilterValue() ?? '');\n  const hasValue = value !== '';\n\n  function clearFilter() {\n    column?.setFilterValue('');\n    inputRef.current?.focus();\n  }\n\n  return (\n    <div className='relative'>\n      <Input\n        id={id}\n        ref={inputRef}\n        type='search'\n        className='peer min-w-60 ps-8 pe-8'\n        value={value}\n        onChange={(event) => {\n          column.setFilterValue(event.currentTarget.value);\n        }}\n        placeholder='Filter by name or email'\n        aria-label='Filter by name or email'\n      />\n      <span className='text-muted-foreground pointer-events-none absolute inset-y-0 start-0 flex items-center ps-2.5 peer-disabled:opacity-50'>\n        <ListFilterIcon aria-hidden='true' className='size-4' />\n      </span>\n      <AnimatePresence initial={false}>\n        {hasValue && (\n          <motion.span\n            key='clear'\n            className='absolute inset-y-0 end-0.5 flex items-center'\n            variants={variants}\n            initial='hidden'\n            animate='shown'\n            exit='hidden'\n          >\n            <Button\n              variant='ghost'\n              size='icon-sm'\n              onClick={clearFilter}\n              aria-label='Clear filter'\n              className='text-muted-foreground hover:text-foreground'\n            >\n              <CircleXIcon />\n            </Button>\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction StatusFilter({ column }: { column: Column<Account> | undefined }) {\n  const id = useId();\n  if (!column) {\n    return null;\n  }\n  const selectedStatuses = statusesOf(column.getFilterValue());\n  const statusCounts = column.getFacetedUniqueValues();\n  const selectedCount = selectedStatuses.length;\n  const hasSelection = selectedCount > 0;\n\n  function changeStatus(status: string, checked: boolean) {\n    const others = selectedStatuses.filter((value) => value !== status);\n    const next = [...others];\n    if (checked) {\n      next.push(status);\n    }\n    const hasNext = next.length > 0;\n    if (!hasNext) {\n      column?.setFilterValue(undefined);\n      return;\n    }\n    column?.setFilterValue(next);\n  }\n\n  return (\n    <Popover>\n      <PopoverTrigger render={<Button variant='outline' />}>\n        <FilterIcon className='-ms-1 opacity-60' />\n        Status\n        {hasSelection && (\n          <span className={COUNT_BADGE}>\n            <MotionNumber value={selectedCount} className='text-foreground' />\n          </span>\n        )}\n      </PopoverTrigger>\n      <PopoverContent align='start' className='w-auto min-w-40 p-3'>\n        <div className='flex flex-col gap-3'>\n          <span className='text-muted-foreground text-xs font-medium'>\n            Filter by status\n          </span>\n          {STATUSES.map((status) => {\n            const checkboxId = `${id}-${status}`;\n            const count = statusCounts.get(status) ?? 0;\n            return (\n              <div key={status} className='flex items-center gap-2'>\n                <Checkbox\n                  id={checkboxId}\n                  checked={selectedStatuses.includes(status)}\n                  onCheckedChange={(checked) => {\n                    changeStatus(status, checked);\n                  }}\n                />\n                <Label\n                  htmlFor={checkboxId}\n                  className='flex grow justify-between gap-4 font-normal'\n                >\n                  {status}\n                  <span className='text-muted-foreground text-xs tabular-nums'>\n                    {count}\n                  </span>\n                </Label>\n              </div>\n            );\n          })}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n\nfunction ColumnToggles({ columns }: { columns: Column<Account>[] }) {\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger render={<Button variant='outline' />}>\n        <Columns3Icon className='-ms-1 opacity-60' />\n        View\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align='end'>\n        <DropdownMenuLabel>Columns</DropdownMenuLabel>\n        {columns.map((column) => (\n          <DropdownMenuCheckboxItem\n            key={column.id}\n            checked={column.getIsVisible()}\n            onCheckedChange={(checked) => {\n              column.toggleVisibility(checked);\n            }}\n          >\n            {labelOf(column)}\n          </DropdownMenuCheckboxItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction DeleteSelected({\n  count,\n  onConfirm,\n}: {\n  count: number;\n  onConfirm: () => void;\n}) {\n  const accounts = accountCountLabel(count);\n\n  return (\n    <AlertDialog>\n      <AlertDialogTrigger render={<Button variant='outline' />}>\n        <TrashIcon className='-ms-1 opacity-60' />\n        Delete\n        <span className={COUNT_BADGE}>\n          <MotionNumber value={count} className='text-foreground' />\n        </span>\n      </AlertDialogTrigger>\n      <AlertDialogContent>\n        <div className='flex flex-col gap-3 max-sm:items-center sm:flex-row sm:gap-4'>\n          <span\n            aria-hidden='true'\n            className='keycap-3d text-destructive-ink flex size-10 shrink-0 items-center justify-center rounded-full'\n          >\n            <CircleAlertIcon className='size-4' />\n          </span>\n          <AlertDialogHeader>\n            <AlertDialogTitle>Delete {accounts}?</AlertDialogTitle>\n            <AlertDialogDescription>\n              The selected {accounts} and their history are removed for the\n              whole team. This cannot be undone.\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n        </div>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction variant='destructive' onClick={onConfirm}>\n            Delete {accounts}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n\nexport default function Table20() {\n  const pageSizeId = useId();\n  const [accounts, setAccounts] = useState(ACCOUNTS);\n  const [sorting, setSorting] = useState<SortingState>([\n    { id: 'name', desc: false },\n  ]);\n  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);\n  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});\n  const [pagination, setPagination] =\n    useState<PaginationState>(INITIAL_PAGINATION);\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const presenceVariants = presenceVariantsOf(shouldReduceMotion);\n\n  function deleteAccount(id: string) {\n    setAccounts((current) => current.filter((account) => account.id !== id));\n  }\n\n  // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table v8 is required and cannot be memoized by the React Compiler.\n  const table = useReactTable({\n    data: accounts,\n    columns: COLUMNS,\n    getRowId: (account) => account.id,\n    state: { sorting, pagination, columnFilters, columnVisibility },\n    onSortingChange: setSorting,\n    onPaginationChange: setPagination,\n    onColumnFiltersChange: setColumnFilters,\n    onColumnVisibilityChange: setColumnVisibility,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getFacetedUniqueValues: getFacetedUniqueValues(),\n    getPaginationRowModel: getPaginationRowModel(),\n    enableSortingRemoval: false,\n  });\n  const rows = table.getRowModel().rows;\n  const hasRows = rows.length > 0;\n  const rowCount = table.getRowCount();\n  const { firstRow, lastRow } = rangeOf(pagination, rowCount);\n  const canGoBack = table.getCanPreviousPage();\n  const canGoForward = table.getCanNextPage();\n  const selectedRows = table.getSelectedRowModel().rows;\n  const selectedCount = selectedRows.length;\n  const hasSelection = selectedCount > 0;\n  const hideableColumns = table\n    .getAllColumns()\n    .filter((column) => column.getCanHide());\n  const visibleColumnCount = table.getVisibleLeafColumns().length;\n\n  function deleteSelected() {\n    const selectedIds = selectedRows.map((row) => row.original.id);\n    setAccounts((current) =>\n      current.filter((account) => !selectedIds.includes(account.id))\n    );\n    table.resetRowSelection();\n  }\n\n  function clearFilters() {\n    table.resetColumnFilters();\n  }\n\n  function changePageSize(size: number | null) {\n    if (size === null) {\n      return;\n    }\n    table.setPageSize(size);\n  }\n\n  return (\n    <DeleteAccountContext.Provider value={deleteAccount}>\n      <div className='flex w-full flex-col gap-4'>\n        <div className='flex flex-wrap items-center justify-between gap-3'>\n          <div className='flex flex-wrap items-center gap-3'>\n            <NameFilter column={table.getColumn('name')} />\n            <StatusFilter column={table.getColumn('status')} />\n            <ColumnToggles columns={hideableColumns} />\n          </div>\n          <div className='flex items-center gap-3'>\n            <AnimatePresence initial={false}>\n              {hasSelection && (\n                <motion.div\n                  key='delete'\n                  variants={presenceVariants}\n                  initial='hidden'\n                  animate='shown'\n                  exit='hidden'\n                >\n                  <DeleteSelected\n                    count={selectedCount}\n                    onConfirm={deleteSelected}\n                  />\n                </motion.div>\n              )}\n            </AnimatePresence>\n            <Button variant='outline'>\n              <PlusIcon className='-ms-1 opacity-60' />\n              Add user\n            </Button>\n          </div>\n        </div>\n        <div className='bg-card hairline-ring ring-border overflow-hidden rounded-xl shadow-[0_1px_2px_rgb(0_0_0/0.04)]'>\n          <Table className='table-fixed'>\n            <TableHeader>\n              {table.getHeaderGroups().map((headerGroup) => (\n                <TableRow key={headerGroup.id} className='hover:bg-transparent'>\n                  {headerGroup.headers.map((header) => (\n                    <TableHead\n                      key={header.id}\n                      aria-sort={ariaSortOf(header.column)}\n                      style={{ width: header.getSize() }}\n                      className={cn('h-11', alignClassOf(header.column.id))}\n                    >\n                      <HeadContent header={header} />\n                    </TableHead>\n                  ))}\n                </TableRow>\n              ))}\n            </TableHeader>\n            <TableBody>\n              {rows.map((row) => (\n                <TableRow\n                  key={row.id}\n                  data-state={rowStateOf(row.getIsSelected())}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell\n                      key={cell.id}\n                      className={cn('last:py-0', alignClassOf(cell.column.id))}\n                    >\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext()\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))}\n              {!hasRows && (\n                <TableRow className='hover:bg-transparent'>\n                  <TableCell\n                    colSpan={visibleColumnCount}\n                    className='h-24 text-center'\n                  >\n                    <div className='flex flex-col items-center gap-1'>\n                      <span className='text-muted-foreground'>\n                        No accounts match these filters.\n                      </span>\n                      <Button variant='link' size='sm' onClick={clearFilters}>\n                        Clear filters\n                      </Button>\n                    </div>\n                  </TableCell>\n                </TableRow>\n              )}\n            </TableBody>\n          </Table>\n        </div>\n        <div className='flex flex-wrap items-center justify-between gap-x-8 gap-y-3'>\n          <div className='flex items-center gap-3'>\n            <Label htmlFor={pageSizeId} className='max-sm:sr-only'>\n              Rows per page\n            </Label>\n            <Select\n              items={PAGE_SIZES}\n              value={pagination.pageSize}\n              onValueChange={changePageSize}\n            >\n              <SelectTrigger id={pageSizeId} className='w-fit tabular-nums'>\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                {PAGE_SIZES.map((size) => (\n                  <SelectItem key={size.value} value={size.value}>\n                    {size.label}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n          <p\n            aria-live='polite'\n            className='text-muted-foreground flex grow justify-end text-[13px] whitespace-nowrap tabular-nums'\n          >\n            <span className='sr-only'>\n              Showing rows {firstRow} to {lastRow} of {rowCount}\n            </span>\n            <span aria-hidden='true' className='inline-flex items-baseline'>\n              <span className='text-foreground inline-flex items-baseline'>\n                <MotionNumber value={firstRow} />\n                –\n                <MotionNumber value={lastRow} />\n              </span>\n              &nbsp;of&nbsp;\n              <span className='text-foreground'>{rowCount}</span>\n            </span>\n          </p>\n          <Pagination className='mx-0 w-auto'>\n            <PaginationContent>\n              <PaginationItem>\n                <Button\n                  size='icon'\n                  variant='outline'\n                  onClick={() => {\n                    table.firstPage();\n                  }}\n                  disabled={!canGoBack}\n                  aria-label='Go to first page'\n                >\n                  <ChevronFirstIcon />\n                </Button>\n              </PaginationItem>\n              <PaginationItem>\n                <Button\n                  size='icon'\n                  variant='outline'\n                  onClick={() => {\n                    table.previousPage();\n                  }}\n                  disabled={!canGoBack}\n                  aria-label='Go to previous page'\n                >\n                  <ChevronLeftIcon />\n                </Button>\n              </PaginationItem>\n              <PaginationItem>\n                <Button\n                  size='icon'\n                  variant='outline'\n                  onClick={() => {\n                    table.nextPage();\n                  }}\n                  disabled={!canGoForward}\n                  aria-label='Go to next page'\n                >\n                  <ChevronRightIcon />\n                </Button>\n              </PaginationItem>\n              <PaginationItem>\n                <Button\n                  size='icon'\n                  variant='outline'\n                  onClick={() => {\n                    table.lastPage();\n                  }}\n                  disabled={!canGoForward}\n                  aria-label='Go to last page'\n                >\n                  <ChevronLastIcon />\n                </Button>\n              </PaginationItem>\n            </PaginationContent>\n          </Pagination>\n        </div>\n      </div>\n    </DeleteAccountContext.Provider>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "table"
  ],
  "type": "registry:component"
}