{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-18",
  "title": "Paginated table",
  "description": "Rows per page, the visible range and buttons from first to last page.",
  "dependencies": [
    "@tanstack/react-table",
    "motion"
  ],
  "registryDependencies": [
    "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/icons.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/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-18.tsx",
      "content": "'use client';\n\nimport {\n  type Column,\n  type ColumnDef,\n  flexRender,\n  getCoreRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  type Header,\n  type PaginationState,\n  type SortDirection,\n  type SortingState,\n  useReactTable,\n} from '@tanstack/react-table';\nimport {\n  AnimatePresence,\n  motion,\n  type Transition,\n  type Variants,\n} from 'motion/react';\nimport { useId, 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} from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Checkbox } from '@/registry/ndeko/ui/checkbox';\nimport { Label } from '@/registry/ndeko/ui/label';\nimport {\n  Pagination,\n  PaginationContent,\n  PaginationItem,\n} from '@/registry/ndeko/ui/pagination';\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: 5 };\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 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  },\n  {\n    header: 'Name',\n    accessorKey: 'name',\n    cell: ({ row }) => <span className='font-medium'>{row.original.name}</span>,\n    size: 180,\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  },\n  {\n    header: 'Balance',\n    accessorKey: 'balance',\n    cell: ({ row }) => CURRENCY.format(row.original.balance),\n    size: 120,\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\nexport default function Table18() {\n  const pageSizeId = useId();\n  const [sorting, setSorting] = useState<SortingState>([\n    { id: 'name', desc: false },\n  ]);\n  const [pagination, setPagination] =\n    useState<PaginationState>(INITIAL_PAGINATION);\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    state: { sorting, pagination },\n    onSortingChange: setSorting,\n    onPaginationChange: setPagination,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n    enableSortingRemoval: false,\n  });\n  const rowCount = table.getRowCount();\n  const { firstRow, lastRow } = rangeOf(pagination, rowCount);\n  const canGoBack = table.getCanPreviousPage();\n  const canGoForward = table.getCanNextPage();\n\n  function changePageSize(size: number | null) {\n    if (size === null) {\n      return;\n    }\n    table.setPageSize(size);\n  }\n\n  return (\n    <div className='flex w-full flex-col gap-4'>\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            {table.getRowModel().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={alignClassOf(cell.column.id)}\n                  >\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                ))}\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  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "table"
  ],
  "type": "registry:component"
}