{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"Animated pagination with spring-based active page indicator","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useId, useMemo } from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\nimport SmoothButton from \"../smooth-button\";\n\nexport type PaginationProps = {\n  /** Current active page (1-indexed). */\n  page: number;\n  /** Total number of pages. */\n  totalPages: number;\n  /** Called when the user selects a different page. */\n  onPageChange: (page: number) => void;\n  /** Number of sibling pages to show on each side of current page. Defaults to 1. */\n  siblings?: number;\n  /** Additional CSS classes for the nav element. */\n  className?: string;\n};\n\nconst ELLIPSIS = \"ellipsis\" as const;\n\ntype PageItem = number | typeof ELLIPSIS;\n\n/** Spring for the sliding active indicator (like animated-tabs) */\nconst SPRING_INDICATOR = {\n  bounce: 0.05,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\n/** Stagger delay per page button on initial render */\nconst STAGGER_DELAY = 0.03;\n\nconst buildPageRange = (\n  page: number,\n  totalPages: number,\n  siblings: number\n): PageItem[] => {\n  const totalSlots = siblings * 2 + 5; // siblings + current + 2 boundary + 2 potential ellipsis\n\n  if (totalPages <= totalSlots) {\n    return Array.from({ length: totalPages }, (_, i) => i + 1);\n  }\n\n  const leftSibling = Math.max(page - siblings, 2);\n  const rightSibling = Math.min(page + siblings, totalPages - 1);\n\n  const showLeftEllipsis = leftSibling > 2;\n  const showRightEllipsis = rightSibling < totalPages - 1;\n\n  const items: PageItem[] = [1];\n\n  if (showLeftEllipsis) {\n    items.push(ELLIPSIS);\n  } else {\n    for (let i = 2; i < leftSibling; i++) {\n      items.push(i);\n    }\n  }\n\n  for (let i = leftSibling; i <= rightSibling; i++) {\n    items.push(i);\n  }\n\n  if (showRightEllipsis) {\n    items.push(ELLIPSIS);\n  } else {\n    for (let i = rightSibling + 1; i < totalPages; i++) {\n      items.push(i);\n    }\n  }\n\n  items.push(totalPages);\n\n  return items;\n};\n\nexport default function Pagination({\n  page,\n  totalPages,\n  onPageChange,\n  siblings = 1,\n  className,\n}: PaginationProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const generatedId = useId();\n  const layoutId = `pagination-active-${generatedId}`;\n\n  const pageItems = useMemo(\n    () => buildPageRange(page, totalPages, siblings),\n    [page, totalPages, siblings]\n  );\n\n  const handlePrev = useCallback(() => {\n    if (page > 1) {\n      onPageChange(page - 1);\n    }\n  }, [page, onPageChange]);\n\n  const handleNext = useCallback(() => {\n    if (page < totalPages) {\n      onPageChange(page + 1);\n    }\n  }, [page, totalPages, onPageChange]);\n\n  return (\n    <nav\n      aria-label=\"Pagination\"\n      className={cn(\"mx-auto flex w-full justify-center\", className)}\n    >\n      <ul className=\"flex flex-row items-center gap-1\">\n        {/* Previous button */}\n        <li>\n          <SmoothButton\n            aria-label=\"Go to previous page\"\n            className=\"gap-1 px-2.5\"\n            disabled={page <= 1}\n            onClick={handlePrev}\n            size=\"sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <ChevronLeft className=\"size-4\" />\n            <span className=\"hidden sm:block\">Previous</span>\n          </SmoothButton>\n        </li>\n\n        {/* Page numbers */}\n        {pageItems.map((item, index) => {\n          if (item === ELLIPSIS) {\n            return (\n              <li\n                aria-hidden=\"true\"\n                className=\"flex size-9 items-center justify-center\"\n                key={`ellipsis-${String(index)}`}\n              >\n                <motion.span\n                  animate={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 1, transform: \"translateY(0px)\" }\n                  }\n                  className=\"text-muted-foreground text-sm\"\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 0, transform: \"translateY(4px)\" }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          ...SPRING_DEFAULT,\n                          delay: index * STAGGER_DELAY,\n                        }\n                  }\n                >\n                  ...\n                </motion.span>\n              </li>\n            );\n          }\n\n          const isActive = item === page;\n\n          return (\n            <motion.li\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 1, transform: \"translateY(0px)\" }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, transform: \"translateY(4px)\" }\n              }\n              key={item}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      ...SPRING_DEFAULT,\n                      delay: index * STAGGER_DELAY,\n                    }\n              }\n            >\n              <SmoothButton\n                aria-current={isActive ? \"page\" : undefined}\n                aria-label={`Go to page ${String(item)}`}\n                className={cn(\n                  \"relative size-9\",\n                  isActive ? \"text-foreground\" : \"text-muted-foreground\"\n                )}\n                onClick={() => onPageChange(item)}\n                size=\"icon\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                {isActive && (\n                  <motion.span\n                    className=\"absolute inset-0 rounded-md border bg-background shadow-sm\"\n                    layout\n                    layoutId={layoutId}\n                    style={{ originY: \"0px\" }}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_INDICATOR\n                    }\n                  />\n                )}\n                <span className=\"relative z-10\">{item}</span>\n              </SmoothButton>\n            </motion.li>\n          );\n        })}\n\n        {/* Next button */}\n        <li>\n          <SmoothButton\n            aria-label=\"Go to next page\"\n            className=\"gap-1 px-2.5\"\n            disabled={page >= totalPages}\n            onClick={handleNext}\n            size=\"sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <span className=\"hidden sm:block\">Next</span>\n            <ChevronRight className=\"size-4\" />\n          </SmoothButton>\n        </li>\n      </ul>\n    </nav>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/pagination/index.tsx","type":"registry:ui"}],"name":"pagination","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/lib.json"],"title":"Pagination","type":"registry:ui"}