{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "type": "registry:ui",
  "title": "Pagination",
  "description": "Move through a long collection with clear bounds.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/pagination/pagination.tsx",
      "content": "\"use client\";\nimport { useLayoutEffect, useRef, useState } from \"react\";\nimport { ChevronLeft as NavArrowLeft, ChevronRight as NavArrowRight } from \"lucide-react\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, type Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n/** Shared by the arrows and the page numbers. */\nconst button =\n  \"relative grid size-control-sm place-items-center rounded-button border border-transparent bg-transparent text-xs/[inherit] text-ink-2 tabular-nums cursor-pointer hover:not-disabled:bg-hover hover:not-disabled:text-ink disabled:cursor-not-allowed disabled:opacity-40 motion-reduce:transition-none\";\n/** Only the arrows press in. Page numbers move with Motion, so they keep CSS off transform and opacity. */\nconst stepButton = cn(\n  button,\n  \"[transition:background-color_var(--duration-fast)_var(--ease-standard),color_var(--duration-fast)_var(--ease-standard),opacity_var(--duration-fast)_var(--ease-standard),transform_var(--duration-spring)_var(--ease-spring)]\",\n  \"active:not-disabled:[transform:scale(.96)] active:not-disabled:[transition-duration:var(--duration-fast),var(--duration-fast),var(--duration-fast),100ms] active:not-disabled:[transition-timing-function:var(--ease-standard),var(--ease-standard),var(--ease-standard),ease-out] motion-reduce:active:not-disabled:[transform:none]\",\n);\n/** Before the shared mark has measured (server render), the current page draws its own; once it has, the mark does and the current page stays clear on hover. */\nconst pageButton = cn(\n  button,\n  \"transition-[background-color,color] duration-fast ease-standard aria-[current=page]:text-ink\",\n  \"[nav:not([data-mark-ready])_&]:aria-[current=page]:border-line [nav:not([data-mark-ready])_&]:aria-[current=page]:bg-surface [nav:not([data-mark-ready])_&]:aria-[current=page]:shadow-resting group-data-mark-ready/pagination:aria-[current=page]:hover:bg-transparent\",\n);\nexport interface PaginationProps {\n  page: number;\n  pageCount: number;\n  onPageChange: (page: number) => void;\n  label?: string;\n}\n/** When the five page window shifts, numbers travel like a belt: each one moves by the same number of slots. */\nconst slot = 1000 / 9; // One button plus the gap (36px + 4px), as a percentage of the button width.\nconst slide: Variants = {\n  enter: (shift: number) => ({ opacity: 0, x: `${slot * shift}%` }),\n  center: { opacity: 1, x: \"0%\" },\n  exit: (shift: number) => ({\n    opacity: 0,\n    x: `${-slot * shift}%`,\n    transition: {\n      x: motionTokens.spring.smooth,\n      opacity: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] },\n    },\n  }),\n};\nexport function Pagination({ page, pageCount, onPageChange, label = \"Pagination\" }: PaginationProps) {\n  const reduced = useReducedMotion() ?? false;\n  const navRef = useRef<HTMLElement>(null);\n  const markX = useMotionValue(0);\n  const markY = useMotionValue(0);\n  const safePageCount = Math.max(0, Math.floor(pageCount));\n  const currentPage = safePageCount > 0 ? Math.min(Math.max(1, Math.floor(page)), safePageCount) : 0;\n  const start = safePageCount > 0 ? Math.max(1, Math.min(safePageCount - 4, currentPage - 2)) : 1;\n  const [previousStart, setPreviousStart] = useState(start);\n  const [shift, setShift] = useState(0);\n  if (previousStart !== start) {\n    setShift(start - previousStart);\n    setPreviousStart(start);\n  }\n  const visible = Array.from({ length: Math.min(safePageCount, 5) }, (_, index) => start + index);\n\n  // One mark travels to the current page's final slot. Until it has measured once (and during server render),\n  // the current button draws its own border so nothing flashes before hydration.\n  useLayoutEffect(() => {\n    const nav = navRef.current;\n    const current = nav?.querySelector<HTMLElement>(\"[aria-current='page']\");\n    if (!nav || !current) return;\n    const place = (instant: boolean) => {\n      const x = current.offsetLeft,\n        y = current.offsetTop;\n      if (instant || reduced) {\n        markX.jump(x);\n        markY.jump(y);\n        return;\n      }\n      animate(markX, x, motionTokens.spring.morph);\n      animate(markY, y, motionTokens.spring.morph);\n    };\n    place(!(\"markReady\" in nav.dataset));\n    nav.dataset.markReady = \"\";\n    let size = `${nav.offsetWidth}x${nav.offsetHeight}`;\n    const observer = new ResizeObserver(() => {\n      const next = `${nav.offsetWidth}x${nav.offsetHeight}`;\n      if (next !== size) {\n        size = next;\n        place(true);\n      }\n    });\n    observer.observe(nav);\n    return () => observer.disconnect();\n  }, [currentPage, start, safePageCount, reduced, markX, markY]);\n\n  if (!safePageCount) return <nav className=\"relative flex flex-wrap items-center gap-1\" aria-label={label} />;\n  return (\n    <nav ref={navRef} className=\"group/pagination relative flex flex-wrap items-center gap-1\" aria-label={label}>\n      <button\n        type=\"button\"\n        className={stepButton}\n        onClick={() => onPageChange(currentPage - 1)}\n        disabled={currentPage <= 1}\n        aria-label=\"Previous page\"\n      >\n        <NavArrowLeft width={16} height={16} aria-hidden=\"true\" />\n      </button>\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute top-0 left-0 box-border size-control-sm rounded-button border border-line bg-surface opacity-0 shadow-resting group-data-mark-ready/pagination:opacity-100\"\n        style={{ x: markX, y: markY }}\n      />\n      <AnimatePresence mode=\"popLayout\" initial={false} custom={shift}>\n        {visible.map((number) => (\n          <motion.button\n            type=\"button\"\n            key={number}\n            className={pageButton}\n            onClick={() => onPageChange(number)}\n            aria-label={`Page ${number}`}\n            aria-current={currentPage === number ? \"page\" : undefined}\n            layout={reduced ? false : \"position\"}\n            layoutDependency={start}\n            custom={shift}\n            variants={slide}\n            initial={reduced ? false : \"enter\"}\n            animate=\"center\"\n            exit={reduced ? undefined : \"exit\"}\n            transition={\n              reduced\n                ? { duration: 0 }\n                : {\n                    x: motionTokens.spring.smooth,\n                    layout: motionTokens.spring.smooth,\n                    opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] },\n                  }\n            }\n          >\n            {number}\n          </motion.button>\n        ))}\n      </AnimatePresence>\n      <button\n        type=\"button\"\n        className={stepButton}\n        onClick={() => onPageChange(currentPage + 1)}\n        disabled={currentPage >= safePageCount}\n        aria-label=\"Next page\"\n      >\n        <NavArrowRight width={16} height={16} aria-hidden=\"true\" />\n      </button>\n    </nav>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/pagination/pagination.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/pagination",
  "meta": {
    "kind": "component",
    "group": "Data display",
    "export": "Pagination",
    "tags": [
      "data",
      "navigation"
    ],
    "docs": "https://irisui.dev/components/pagination"
  }
}
