{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "type": "registry:ui",
  "title": "Pagination",
  "description": "Numbered page links with previous/next. The current page has aria-current=\"page\".",
  "registryDependencies": [
    "https://junco.madebytobi.com/r/button.json",
    "https://junco.madebytobi.com/r/icons.json",
    "https://junco.madebytobi.com/r/junco-theme.json",
    "https://junco.madebytobi.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/junco/ui/pagination.tsx",
      "type": "registry:ui",
      "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from \"@/lib/junco/icons\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nexport type PaginationItem = number | \"ellipsis-start\" | \"ellipsis-end\";\n\nexport interface UsePaginationOptions {\n  /** 1-based current page. */\n  page: number;\n  /** Total number of pages (at least 1). */\n  pageCount: number;\n  /** Pages shown on each side of the current one. Default 1. */\n  siblings?: number;\n  /** Pages always shown at each end. Default 1. */\n  boundaries?: number;\n}\n\n/**\n * The list of page numbers and ellipses to render. Always returns the same number of items\n * once there are enough pages, so the control doesn't change width as you page.\n *\n *   usePagination({ page: 6, pageCount: 20 }) // [1, \"ellipsis-start\", 5, 6, 7, \"ellipsis-end\", 20]\n */\nexport function getPaginationItems({ page, pageCount, siblings = 1, boundaries = 1 }: UsePaginationOptions): PaginationItem[] {\n  const total = Math.max(1, Math.floor(pageCount));\n  const current = Math.min(Math.max(1, Math.floor(page)), total);\n  const range = (from: number, to: number) => Array.from({ length: Math.max(0, to - from + 1) }, (_, i) => from + i);\n\n  // boundaries on both ends + siblings on both sides + current + two ellipses\n  const slots = boundaries * 2 + siblings * 2 + 3;\n  if (total <= slots) return range(1, total);\n\n  const start = range(1, boundaries);\n  const end = range(total - boundaries + 1, total);\n  const siblingsStart = Math.max(Math.min(current - siblings, total - boundaries - siblings * 2 - 1), boundaries + 2);\n  const siblingsEnd = Math.min(Math.max(current + siblings, boundaries + siblings * 2 + 2), total - boundaries - 1);\n\n  return [\n    ...start,\n    ...(siblingsStart > boundaries + 2 ? ([\"ellipsis-start\"] as const) : siblingsStart === boundaries + 2 ? [boundaries + 1] : []),\n    ...range(siblingsStart, siblingsEnd),\n    ...(siblingsEnd < total - boundaries - 1 ? ([\"ellipsis-end\"] as const) : siblingsEnd === total - boundaries - 1 ? [total - boundaries] : []),\n    ...end,\n  ];\n}\n\nexport function usePagination(options: UsePaginationOptions) {\n  const { page, pageCount, siblings, boundaries } = options;\n  const items = React.useMemo(\n    () => getPaginationItems({ page, pageCount, siblings, boundaries }),\n    [page, pageCount, siblings, boundaries],\n  );\n  const total = Math.max(1, pageCount);\n  return { items, hasPrevious: page > 1, hasNext: page < total, pageCount: total };\n}\n\nexport interface PaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, \"onChange\"> {\n  page: number;\n  pageCount: number;\n  onPageChange: (page: number) => void;\n  siblings?: number;\n  boundaries?: number;\n  /** Builds a URL per page so each page is a real link (better for SEO and middle-click). */\n  getHref?: (page: number) => string;\n  size?: \"sm\" | \"md\";\n  /** Hide the numbered pages (prev/next + \"Page x of y\"), e.g. on narrow screens. */\n  isCompact?: boolean;\n  /** Accessible name for the nav. Default \"Pagination\". */\n  label?: string;\n  classNames?: Partial<Record<\"root\" | \"list\" | \"item\" | \"ellipsis\" | \"summary\", string>>;\n}\n\n/** Numbered page links with previous/next. The current page has aria-current=\"page\". */\nexport const Pagination = React.forwardRef<HTMLElement, PaginationProps>(\n  (\n    {\n      className,\n      classNames,\n      page,\n      pageCount,\n      onPageChange,\n      siblings,\n      boundaries,\n      getHref,\n      size = \"md\",\n      isCompact = false,\n      label = \"Pagination\",\n      ...props\n    },\n    ref,\n  ) => {\n    const { items, hasPrevious, hasNext, pageCount: total } = usePagination({ page, pageCount, siblings, boundaries });\n    const btnSize = size === \"sm\" ? \"sm\" : \"md\";\n    // On phones the md size steps down so prev + 7 pages + next fit in one row at 360px.\n    const square = size === \"sm\" ? \"min-w-8 px-1.5\" : \"min-w-9 px-2 max-sm:h-8 max-sm:min-w-8 max-sm:px-1.5\";\n\n    const go = (target: number) => (event: React.MouseEvent) => {\n      // let modified clicks open links in a new tab\n      if (getHref && (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1)) return;\n      event.preventDefault();\n      if (target !== page) onPageChange(target);\n    };\n\n    // A render function, not a component: a component defined here would remount on every\n    // render and drop keyboard focus after \"Next\".\n    const pageLink = ({\n      target,\n      disabled,\n      children,\n      className: extra,\n      ...rest\n    }: { target: number; disabled?: boolean; children: React.ReactNode } & React.AriaAttributes & { className?: string }) => {\n      const cls = cn(buttonVariants({ variant: \"ghost\", size: btnSize }), \"tabular-nums\", extra);\n      if (disabled) {\n        return (\n          <span aria-disabled=\"true\" className={cn(cls, \"pointer-events-none opacity-50\")} {...rest}>\n            {children}\n          </span>\n        );\n      }\n      return getHref ? (\n        <a href={getHref(target)} onClick={go(target)} className={cls} {...rest}>\n          {children}\n        </a>\n      ) : (\n        <button type=\"button\" onClick={go(target)} className={cls} {...rest}>\n          {children}\n        </button>\n      );\n    };\n\n    return (\n      <nav\n        ref={ref}\n        aria-label={label}\n        data-slot=\"pagination\"\n        className={cn(\"flex items-center\", classNames?.root, className)}\n        {...props}\n      >\n        <ul className={cn(\"flex items-center gap-1 max-sm:gap-0.5\", classNames?.list)}>\n          <li>\n            {pageLink({\n              target: page - 1,\n              disabled: !hasPrevious,\n              \"aria-label\": \"Previous page\",\n              className: cn(\"gap-1 pr-3 pl-2\", !isCompact && \"max-sm:size-8 max-sm:px-0\"),\n              children: (\n                <>\n                  <ChevronLeftIcon className=\"size-4\" />\n                  <span className={isCompact ? \"sr-only\" : \"max-sm:sr-only\"}>Previous</span>\n                </>\n              ),\n            })}\n          </li>\n          {isCompact ? (\n            <li className={cn(\"px-2 text-sm text-muted-foreground tabular-nums\", classNames?.summary)} aria-live=\"polite\">\n              Page {Math.min(page, total)} of {total}\n            </li>\n          ) : (\n            items.map((item) =>\n              typeof item === \"number\" ? (\n                <li key={item} className={classNames?.item}>\n                  {pageLink({\n                    target: item,\n                    \"aria-label\": `Page ${item}`,\n                    \"aria-current\": item === page ? \"page\" : undefined,\n                    className: cn(square, item === page && \"border-border bg-surface text-foreground hover:bg-surface\"),\n                    children: item,\n                  })}\n                </li>\n              ) : (\n                <li\n                  key={item}\n                  aria-hidden\n                  className={cn(\"grid place-items-center text-muted-foreground\", square, classNames?.ellipsis)}\n                >\n                  <MoreHorizontalIcon className=\"size-4\" />\n                </li>\n              ),\n            )\n          )}\n          <li>\n            {pageLink({\n              target: page + 1,\n              disabled: !hasNext,\n              \"aria-label\": \"Next page\",\n              className: cn(\"gap-1 pr-2 pl-3\", !isCompact && \"max-sm:size-8 max-sm:px-0\"),\n              children: (\n                <>\n                  <span className={isCompact ? \"sr-only\" : \"max-sm:sr-only\"}>Next</span>\n                  <ChevronRightIcon className=\"size-4\" />\n                </>\n              ),\n            })}\n          </li>\n        </ul>\n      </nav>\n    );\n  },\n);\nPagination.displayName = \"Pagination\";\n"
    }
  ],
  "docs": "Docs and examples: https://junco.madebytobi.com/docs/components/pagination",
  "meta": {
    "hash": "1712f3d7e86a96a7",
    "source": "packages/react/src/components/pagination.tsx",
    "exports": [
      "PaginationItem",
      "UsePaginationOptions",
      "getPaginationItems",
      "usePagination",
      "PaginationProps",
      "Pagination"
    ],
    "props": {
      "Pagination": {
        "extends": "Omit<React.HTMLAttributes<HTMLElement>, \"onChange\">",
        "props": [
          {
            "name": "page",
            "optional": false,
            "type": "number"
          },
          {
            "name": "pageCount",
            "optional": false,
            "type": "number"
          },
          {
            "name": "onPageChange",
            "optional": false,
            "type": "(page: number) => void"
          },
          {
            "name": "siblings",
            "optional": true,
            "type": "number"
          },
          {
            "name": "boundaries",
            "optional": true,
            "type": "number"
          },
          {
            "name": "getHref",
            "optional": true,
            "type": "(page: number) => string",
            "description": "Builds a URL per page so each page is a real link (better for SEO and middle-click)."
          },
          {
            "name": "size",
            "optional": true,
            "type": "\"sm\" | \"md\""
          },
          {
            "name": "isCompact",
            "optional": true,
            "type": "boolean",
            "description": "Hide the numbered pages (prev/next + \"Page x of y\"), e.g. on narrow screens."
          },
          {
            "name": "label",
            "optional": true,
            "type": "string",
            "description": "Accessible name for the nav. Default \"Pagination\"."
          },
          {
            "name": "classNames",
            "optional": true,
            "type": "Partial<Record<\"root\" | \"list\" | \"item\" | \"ellipsis\" | \"summary\""
          }
        ]
      }
    },
    "usage": "usePagination({ page: 6, pageCount: 20 }) // [1, \"ellipsis-start\", 5, 6, 7, \"ellipsis-end\", 20]",
    "docsUrl": "https://junco.madebytobi.com/docs/components/pagination"
  }
}
