{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "pagination-controls", "title": "Pagination Controls", "description": "Controlled one-indexed pagination with compact and numbered-page variants.", "registryDependencies": [ "askyourpolicy/ss-registry/stitch-base", "askyourpolicy/ss-registry/button", "askyourpolicy/ss-registry/native-select" ], "files": [ { "path": "src/components/ui/pagination-controls.tsx", "content": "\"use client\";\n\nimport { CaretLeftIcon, CaretRightIcon, DotsThreeIcon } from \"@phosphor-icons/react\";\nimport { useId, type ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { NativeSelect, NativeSelectOption } from \"@/components/ui/native-select\";\nimport { cn } from \"@/lib/utils\";\n\ntype PaginationVariant = \"compact\" | \"pages\";\ntype PaginationItem = number | \"ellipsis\";\n\ntype PaginationRange = {\n firstItem: number;\n lastItem: number;\n page: number;\n pageCount: number;\n totalItems: number;\n};\n\ntype PaginationControlsProps = Omit, \"onChange\"> & {\n onPageChange: (page: number) => void;\n onPageSizeChange?: (pageSize: number) => void;\n page: number;\n pageSize: number;\n pageSizeOptions?: readonly number[];\n siblingCount?: number;\n totalItems: number;\n variant?: PaginationVariant;\n};\n\nfunction getPageCount(totalItems: number, pageSize: number) {\n const safeTotal = toNonNegativeInteger(totalItems);\n const safePageSize = toPositiveInteger(pageSize);\n return safeTotal === 0 ? 0 : Math.ceil(safeTotal / safePageSize);\n}\n\nfunction clampPage(page: number, pageCount: number) {\n const safePageCount = toNonNegativeInteger(pageCount);\n if (safePageCount === 0) return 1;\n\n const integerPage = Number.isFinite(page) ? Math.floor(page) : 1;\n return Math.min(Math.max(integerPage, 1), safePageCount);\n}\n\nfunction getPaginationRange(page: number, pageSize: number, totalItems: number): PaginationRange {\n const safeTotal = toNonNegativeInteger(totalItems);\n const safePageSize = toPositiveInteger(pageSize);\n const pageCount = getPageCount(safeTotal, safePageSize);\n const safePage = clampPage(page, pageCount);\n\n if (safeTotal === 0) {\n return {\n firstItem: 0,\n lastItem: 0,\n page: safePage,\n pageCount,\n totalItems: safeTotal,\n };\n }\n\n return {\n firstItem: (safePage - 1) * safePageSize + 1,\n lastItem: Math.min(safePage * safePageSize, safeTotal),\n page: safePage,\n pageCount,\n totalItems: safeTotal,\n };\n}\n\nfunction getPaginationItems(page: number, pageCount: number, siblingCount = 1): PaginationItem[] {\n const safePageCount = toNonNegativeInteger(pageCount);\n if (safePageCount === 0) return [];\n\n const safePage = clampPage(page, safePageCount);\n const safeSiblingCount = toNonNegativeInteger(siblingCount);\n const visibleItemCount = safeSiblingCount * 2 + 5;\n\n if (safePageCount <= visibleItemCount) {\n return createRange(1, safePageCount);\n }\n\n const leftSiblingPage = Math.max(safePage - safeSiblingCount, 1);\n const rightSiblingPage = Math.min(safePage + safeSiblingCount, safePageCount);\n const showLeftEllipsis = leftSiblingPage > 3;\n const showRightEllipsis = rightSiblingPage < safePageCount - 2;\n\n if (!showLeftEllipsis && showRightEllipsis) {\n const leftRange = createRange(1, 3 + safeSiblingCount * 2);\n return [...leftRange, \"ellipsis\", safePageCount];\n }\n\n if (showLeftEllipsis && !showRightEllipsis) {\n const rightRange = createRange(safePageCount - (2 + safeSiblingCount * 2), safePageCount);\n return [1, \"ellipsis\", ...rightRange];\n }\n\n return [\n 1,\n \"ellipsis\",\n ...createRange(leftSiblingPage, rightSiblingPage),\n \"ellipsis\",\n safePageCount,\n ];\n}\n\nfunction PaginationControls({\n \"aria-label\": ariaLabel = \"Pagination\",\n className,\n onPageChange,\n onPageSizeChange,\n page,\n pageSize,\n pageSizeOptions,\n siblingCount = 1,\n totalItems,\n variant = \"pages\",\n ...props\n}: PaginationControlsProps) {\n const pageSizeId = useId();\n const range = getPaginationRange(page, pageSize, totalItems);\n const safePageSize = toPositiveInteger(pageSize);\n const options = getPageSizeOptions(safePageSize, pageSizeOptions);\n const canGoPrevious = range.pageCount > 0 && range.page > 1;\n const canGoNext = range.pageCount > 0 && range.page < range.pageCount;\n\n return (\n \n \n {range.totalItems === 0\n ? \"0 items\"\n : `${range.firstItem}–${range.lastItem} of ${range.totalItems}`}\n \n
\n {onPageSizeChange && options.length > 0 ? (\n
\n \n onPageSizeChange(Number(event.currentTarget.value))}\n size=\"sm\"\n value={String(safePageSize)}\n >\n {options.map((option) => (\n \n {option}\n \n ))}\n \n
\n ) : null}\n
    \n
  • \n onPageChange(clampPage(range.page - 1, range.pageCount))}\n size=\"icon-sm\"\n type=\"button\"\n variant=\"outline\"\n >\n \n \n
  • \n {variant === \"pages\"\n ? getPaginationItems(range.page, range.pageCount, siblingCount).map((item, index) => (\n
  • \n {item === \"ellipsis\" ? (\n \n \n \n ) : (\n onPageChange(item)}\n size=\"icon-sm\"\n type=\"button\"\n variant={item === range.page ? \"outline\" : \"ghost\"}\n >\n {item}\n \n )}\n
  • \n ))\n : null}\n
  • \n onPageChange(clampPage(range.page + 1, range.pageCount))}\n size=\"icon-sm\"\n type=\"button\"\n variant=\"outline\"\n >\n \n \n
  • \n
\n
\n \n );\n}\n\nfunction createRange(start: number, end: number) {\n return Array.from({ length: Math.max(end - start + 1, 0) }, (_, index) => start + index);\n}\n\nfunction toNonNegativeInteger(value: number) {\n if (!Number.isFinite(value)) return 0;\n return Math.max(0, Math.floor(value));\n}\n\nfunction toPositiveInteger(value: number) {\n if (!Number.isFinite(value) || value <= 0) return 1;\n return Math.max(1, Math.floor(value));\n}\n\nfunction getPageSizeOptions(pageSize: number, options?: readonly number[]) {\n if (!options) return [];\n\n const normalizedOptions = options\n .filter((option) => Number.isFinite(option) && option > 0)\n .map((option) => Math.floor(option));\n\n return [...new Set([pageSize, ...normalizedOptions])];\n}\n\nexport {\n PaginationControls,\n clampPage,\n getPageCount,\n getPaginationItems,\n getPaginationRange,\n type PaginationControlsProps,\n type PaginationItem,\n type PaginationRange,\n type PaginationVariant,\n};\n", "type": "registry:ui" } ], "type": "registry:ui" }