{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "title": "Pagination",
  "description": "A pagination component.",
  "dependencies": [
    "class-variance-authority",
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/button"
  ],
  "files": [
    {
      "path": "registry/default/pagination/pagination.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  MoreHorizontalIcon,\n} from \"@hugeicons/core-free-icons\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"@/registry/default/button/button\";\nimport \"./pagination.css\";\n\nexport {\n  getPaginationRange,\n  type PaginationRangeItem,\n  type PaginationRangeOptions,\n} from \"./lib/range\";\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n  return (\n    <nav\n      aria-label=\"Pagination\"\n      data-slot=\"pagination\"\n      className={cn(\"mx-auto flex w-full justify-center\", className)}\n      {...props}\n    />\n  );\n}\n\n// The list is a shell, as in Button Group, with a current-page fill that\n// glides between links. Insets are link borders (pagination.css); plate\n// corners are the shell's minus the 2px edge border.\nconst paginationContentVariants = cva(\n  [\n    \"relative isolate flex w-fit flex-row items-stretch rounded-(--pagination-radius)\",\n    \"[--pagination-item-radius:max(0px,calc(var(--pagination-radius)-2px))]\",\n    // Fixed shell; links stretch to fill it\n    \"h-(--pagination-h)\",\n    // Current page colors (override to recolor)\n    \"[--pagination-current-bg:var(--neutral)] [--pagination-current-fg:var(--neutral-foreground)]\",\n  ],\n  {\n    variants: {\n      variant: {\n        // Outline draws its line as an outline just outside the shell.\n        outline: \"bg-card outline outline-border\",\n        soft: \"bg-secondary text-secondary-foreground\",\n        ghost: \"\",\n      },\n      // Button's height ladder (one step taller below sm); corners step with it.\n      size: {\n        sm: \"[--pagination-h:--spacing(9)] sm:not-pointer-coarse:[--pagination-h:--spacing(8)] [--pagination-radius:calc(var(--radius)-2px)]\",\n        default:\n          \"[--pagination-h:--spacing(10)] sm:not-pointer-coarse:[--pagination-h:--spacing(9)] [--pagination-radius:var(--radius)]\",\n        lg: \"[--pagination-h:--spacing(11)] sm:not-pointer-coarse:[--pagination-h:--spacing(10)] [--pagination-radius:calc(var(--radius)+2px)]\",\n      },\n    },\n    defaultVariants: {\n      variant: \"outline\",\n      size: \"default\",\n    },\n  },\n);\n\ntype PaginationContentProps = React.ComponentProps<\"ul\"> &\n  VariantProps<typeof paginationContentVariants>;\n\nfunction PaginationContent({\n  className,\n  variant,\n  size,\n  children,\n  ...props\n}: PaginationContentProps) {\n  return (\n    <ul\n      data-slot=\"pagination-content\"\n      data-variant={variant ?? \"outline\"}\n      data-size={size ?? \"default\"}\n      className={cn(paginationContentVariants({ variant, size }), className)}\n      {...props}\n    >\n      {children}\n      {/* Last, so first-child selectors still match the first item. */}\n      <li aria-hidden data-slot=\"pagination-indicator\" />\n    </ul>\n  );\n}\n\nfunction PaginationItem({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      data-slot=\"pagination-item\"\n      className={cn(\"flex\", className)}\n      {...props}\n    />\n  );\n}\n\ntype PaginationLinkProps = useRender.ComponentProps<\"a\"> & {\n  /** Marks the link as the current page. */\n  isActive?: boolean;\n  /** Turns the link off, for Previous on the first page or Next on the last. */\n  isDisabled?: boolean;\n};\n\n// Real anchors with the flat `buttonVariants` recipe: <Button render={<a/>}>\n// would add button semantics to elements that must stay links.\nconst linkClasses = cn(\n  buttonVariants({ variant: \"ghost\", size: null }),\n  // The plate (content box, no padding) is square; wider labels grow it.\n  \"box-content aspect-square h-auto self-stretch px-0 text-sm tabular-nums\",\n  // Box corners are the plate's + 2px; the focus ring is in pagination.css.\n  \"rounded-[calc(var(--pagination-item-radius)+2px)] before:rounded-(--pagination-item-radius)\",\n  // Hover and press: the label color at 10% / 14%\n  \"[--btn-bg-hover:color-mix(in_oklab,currentColor_10%,transparent)] [--btn-bg-active:color-mix(in_oklab,currentColor_14%,transparent)]\",\n  // Current page fallback fill, where the indicator can't glide\n  \"aria-[current=page]:text-(--pagination-current-fg) aria-[current=page]:hover:text-(--pagination-current-fg) aria-[current=page]:[--btn-bg:var(--pagination-current-bg)] aria-[current=page]:[--btn-bg-hover:var(--pagination-current-bg)] aria-[current=page]:[--btn-bg-active:var(--pagination-current-bg)]\",\n  \"aria-disabled:pointer-events-none aria-disabled:opacity-60\",\n  // No transitions on the link: Chrome skips the indicator's slide when the\n  // new current link starts one. Hover is instant; the press tint still\n  // fades (Button's 100ms).\n  \"transition-[outline-width,outline-offset,outline-color] before:[transition-duration:0ms,100ms,100ms] active:before:[transition-duration:100ms] aria-[current=page]:before:transition-none\",\n);\n\nfunction PaginationLink({\n  className,\n  isActive,\n  isDisabled,\n  render,\n  children,\n  href,\n  onClick,\n  ...props\n}: PaginationLinkProps) {\n  const defaultProps = {\n    \"aria-current\": isActive ? (\"page\" as const) : undefined,\n    \"aria-disabled\": isDisabled || undefined,\n    // No href, so a screen reader's virtual cursor can't follow it.\n    href: isDisabled ? undefined : href,\n    role: isDisabled ? \"link\" : undefined,\n    onClick: (event: React.MouseEvent<HTMLAnchorElement>) => {\n      if (isDisabled) {\n        event.preventDefault();\n        return;\n      }\n      onClick?.(event);\n    },\n    tabIndex: isDisabled ? -1 : undefined,\n    \"data-slot\": \"pagination-link\",\n    \"data-active\": isActive || undefined,\n    \"data-disabled\": isDisabled || undefined,\n    className: cn(linkClasses, className),\n    children: (\n      <span\n        data-slot=\"pagination-link-label\"\n        className=\"inline-flex items-center gap-[inherit]\"\n      >\n        {children}\n      </span>\n    ),\n  };\n\n  return useRender({\n    defaultTagName: \"a\",\n    render,\n    props: mergeProps<\"a\">(defaultProps, props),\n  });\n}\n\n// Below sm the label is sr-only; it stays the accessible name.\nfunction PaginationPrevious({\n  className,\n  children = \"Previous\",\n  ...props\n}: PaginationLinkProps) {\n  return (\n    <PaginationLink\n      className={cn(\"gap-1.5 sm:ps-2 sm:pe-3\", className)}\n      {...props}\n    >\n      <HugeiconsIcon icon={ChevronLeftIcon} strokeWidth={2} aria-hidden />\n      <span className=\"max-sm:sr-only\">{children}</span>\n    </PaginationLink>\n  );\n}\n\nfunction PaginationNext({\n  className,\n  children = \"Next\",\n  ...props\n}: PaginationLinkProps) {\n  return (\n    <PaginationLink\n      className={cn(\"gap-1.5 sm:ps-3 sm:pe-2\", className)}\n      {...props}\n    >\n      <span className=\"max-sm:sr-only\">{children}</span>\n      <HugeiconsIcon icon={ChevronRightIcon} strokeWidth={2} aria-hidden />\n    </PaginationLink>\n  );\n}\n\nfunction PaginationEllipsis({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"pagination-ellipsis\"\n      className={cn(\n        \"text-muted-foreground flex aspect-square items-center justify-center self-stretch [&_svg]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <HugeiconsIcon icon={MoreHorizontalIcon} strokeWidth={2} aria-hidden />\n      <span className=\"sr-only\">More pages</span>\n    </span>\n  );\n}\n\n// Read-only position for compact pagers, announced when it changes.\nfunction PaginationStatus({ className, ...props }: React.ComponentProps<\"li\">) {\n  return (\n    <li\n      aria-live=\"polite\"\n      aria-atomic\n      data-slot=\"pagination-status\"\n      className={cn(\n        \"flex items-center justify-center px-2.5 text-sm font-medium whitespace-nowrap tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationEllipsis,\n  PaginationItem,\n  PaginationLink,\n  PaginationNext,\n  PaginationPrevious,\n  PaginationStatus,\n  paginationContentVariants,\n};\n\nexport type { PaginationContentProps, PaginationLinkProps };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/pagination/pagination.tsx"
    },
    {
      "path": "registry/default/pagination/lib/range.ts",
      "content": "export type PaginationRangeItem = number | \"start-ellipsis\" | \"end-ellipsis\";\n\nexport type PaginationRangeOptions = {\n  /** The current page, 1-based. Clamped to `1..pageCount`. */\n  page: number;\n  /** How many pages there are. */\n  pageCount: number;\n  /** Pages shown on each side of the current one. Defaults to 1. */\n  siblingCount?: number;\n  /** Pages always shown at the start and the end. Defaults to 1. */\n  boundaryCount?: number;\n};\n\nfunction range(start: number, end: number): number[] {\n  const length = end - start + 1;\n  return length > 0 ? Array.from({ length }, (_, i) => start + i) : [];\n}\n\n/**\n * The pages and ellipses to render for `page` of `pageCount`. The length is\n * fixed for given counts (an ellipsis that would hide one page shows it), so\n * the row keeps its width as the page changes.\n */\nexport function getPaginationRange({\n  page,\n  pageCount,\n  siblingCount = 1,\n  boundaryCount = 1,\n}: PaginationRangeOptions): PaginationRangeItem[] {\n  // NaN (e.g. from Number(searchParams.get(\"page\"))) means no pages or page 1.\n  const count = Number.isFinite(pageCount)\n    ? Math.max(0, Math.floor(pageCount))\n    : 0;\n  if (count === 0) return [];\n  const current = Number.isFinite(page)\n    ? Math.min(Math.max(1, Math.floor(page)), count)\n    : 1;\n  const siblings = Math.max(0, Math.floor(siblingCount));\n  const boundaries = Math.max(0, Math.floor(boundaryCount));\n\n  // Everything fits, ellipsis slots included.\n  if (count <= boundaries * 2 + siblings * 2 + 3) return range(1, count);\n\n  // From here the boundary runs can't overlap.\n  const startPages = range(1, boundaries);\n  const endPages = range(count - boundaries + 1, count);\n\n  // Sibling window, pushed inward at the ends so the length stays fixed.\n  const siblingsStart = Math.max(\n    Math.min(current - siblings, count - boundaries - siblings * 2 - 1),\n    boundaries + 2,\n  );\n  const siblingsEnd = Math.min(\n    Math.max(current + siblings, boundaries + siblings * 2 + 2),\n    count - boundaries - 1,\n  );\n\n  return [\n    ...startPages,\n    siblingsStart > boundaries + 2 ? \"start-ellipsis\" : boundaries + 1,\n    ...range(siblingsStart, siblingsEnd),\n    siblingsEnd < count - boundaries - 1 ? \"end-ellipsis\" : count - boundaries,\n    ...endPages,\n  ];\n}\n",
      "type": "registry:lib",
      "target": "components/ui/cubby-ui/pagination/lib/range.ts"
    },
    {
      "path": "registry/default/pagination/pagination.css",
      "content": "/* -------------------------------------------------------------------------------------------------\n * Inset: link borders, 2px facing the shell's edge, 1px facing a neighbour.\n * The ends are the first and last list items; the indicator is skipped.\n * -------------------------------------------------------------------------------------------------*/\n\n[data-slot=\"pagination-content\"] [data-slot=\"pagination-link\"] {\n  border: 1px solid transparent;\n  border-block-width: 2px;\n}\n\n/* A square hit area over the whole box, corners included, so hover never\n * drops between links. */\n[data-slot=\"pagination-content\"] [data-slot=\"pagination-link\"]::after {\n  content: \"\";\n  position: absolute;\n  inset-block: -2px;\n  inset-inline: -1px;\n}\n\n[data-slot=\"pagination-content\"]\n  > :nth-child(1 of :not([data-slot=\"pagination-indicator\"]))\n  > [data-slot=\"pagination-link\"] {\n  border-inline-start-width: 2px;\n}\n\n[data-slot=\"pagination-content\"]\n  > :nth-last-child(1 of :not([data-slot=\"pagination-indicator\"]))\n  > [data-slot=\"pagination-link\"] {\n  border-inline-end-width: 2px;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Gliding current page, in CSS anchor positioning.\n *\n * The current link's plate (its ::before, inside the border) takes the\n * anchor name, so the fill matches the plate whatever the link's border\n * widths.\n * Without anchor positioning and anchor-scope, the current link paints its\n * own fill instead.\n * -------------------------------------------------------------------------------------------------*/\n\n[data-slot=\"pagination-indicator\"] {\n  display: none;\n}\n\n/* Hover is instant. Only the label of the page becoming current fades,\n * after the fill (below). */\n\n/* All anchor features or none: a partial implementation leaves the fill\n * unplaced or tied to another list. */\n@supports (anchor-name: --pagination-current) and\n  (anchor-scope: --pagination-current) and (inset: anchor(inside)) {\n  [data-slot=\"pagination-content\"] {\n    anchor-scope: --pagination-current;\n  }\n\n  [data-slot=\"pagination-link\"][aria-current=\"page\"]::before {\n    anchor-name: --pagination-current;\n  }\n\n  [data-slot=\"pagination-link\"][aria-current=\"page\"] {\n    --btn-bg: transparent;\n    --btn-bg-hover: transparent;\n    --btn-bg-active: transparent;\n  }\n\n  /* The new label waits for the fill so it never sits light on the shell. */\n  [data-slot=\"pagination-link\"][aria-current=\"page\"]\n    > [data-slot=\"pagination-link-label\"] {\n    transition: color 100ms ease-out 70ms;\n  }\n\n  [data-slot=\"pagination-indicator\"] {\n    display: block;\n    position: absolute;\n    position-anchor: --pagination-current;\n    inset: anchor(inside);\n    border-radius: var(--pagination-item-radius);\n    background-color: var(--pagination-current-bg);\n    z-index: -1;\n    pointer-events: none;\n    /* On first paint the anchor can't interpolate, so the fill starts in\n     * place. */\n    transition: inset 200ms var(--ease-out-expo, cubic-bezier(0.19, 1, 0.22, 1));\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"pagination-indicator\"] {\n      transition: none;\n    }\n\n    [data-slot=\"pagination-link\"][aria-current=\"page\"]\n      > [data-slot=\"pagination-link-label\"] {\n      transition-delay: 0s;\n    }\n  }\n}\n\n/* Forced colors drop the fill, so the current page gets a Highlight border. */\n@media (forced-colors: active) {\n  [data-slot=\"pagination-link\"][aria-current=\"page\"] {\n    border-color: Highlight;\n  }\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Focus ring: 2px inside the fill's edge, on the plate (::before), following\n * its corners.\n * -------------------------------------------------------------------------------------------------*/\n\n[data-slot=\"pagination-content\"] [data-slot=\"pagination-link\"]:focus-visible {\n  outline-color: transparent;\n}\n\n[data-slot=\"pagination-content\"]\n  [data-slot=\"pagination-link\"]:focus-visible::before {\n  outline: 2px solid color-mix(in oklab, var(--color-ring) 50%, transparent);\n  outline-offset: -2px;\n}\n",
      "type": "registry:file",
      "target": "components/ui/cubby-ui/pagination/pagination.css"
    }
  ],
  "type": "registry:ui"
}