{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "type": "registry:component",
  "title": "Pagination",
  "description": "Page controls for a long result set: the pages near you, the gaps between them, and a way across.",
  "dependencies": [
    "tailwind-variants"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/panelui/pagination/index.tsx",
      "type": "registry:component",
      "target": "@components/panelui/pagination/index.tsx",
      "content": "'use client';\n\n/**\n * Pagination — moving through a result set one page at a time.\n *\n * ```tsx\n * <Pagination count={12} page={page} onPageChange={setPage} />\n * ```\n *\n * The component owns the arithmetic and nothing else. Give it how many pages\n * there are and which one you are on, and it works out which numbers to show,\n * where the gaps fall, and which controls are dead at the ends; it never\n * fetches and never slices, because the data is yours.\n *\n * ## Three shapes\n *\n * `numbers` is the full run, with the middle folded into gaps. It is the one\n * to reach for on a search result or a table: the page you are on sits among\n * its neighbours, and the two ends stay visible while you move.\n *\n * `compact` puts the two arrows either side of `3 / 12`. Digits and a slash\n * rather than a sentence, so the shape is the same in any language and fits a\n * table footer or a phone.\n *\n * `simple` replaces the numbers with the words Previous and Next. Use it for\n * a flow you walk rather than jump around in: a wizard, a set of onboarding\n * cards, the pages of an article. It says where the flow goes, which a row of\n * numbers does not.\n *\n * ## The run holds its width\n *\n * The visible run is a fixed width, not a window that grows and shrinks, so\n * the arrows stay where the hand left them as you step through the pages.\n * `siblings` sets how many pages sit either side of the current one and\n * `boundaries` how many are pinned at each end. A gap of exactly one page is\n * drawn as that page, because a gap marker wider than the number it hides is\n * a worse trade than the number.\n *\n * An ellipsis is a button, not punctuation. A dead target in the middle of a\n * row of live ones is a thing people press and then think is broken, so\n * pressing one jumps `pageJump` pages towards the end the gap is on.\n *\n * ## With something beside it\n *\n * Given children, the root becomes a row with them on the leading edge and\n * the controls on the trailing one. That is where `Pagination.Status` goes:\n *\n * ```tsx\n * <Pagination count={8} page={page} onPageChange={setPage}>\n *   <Pagination.Status page={page} pageSize={20} total={143} />\n * </Pagination>\n * ```\n *\n * `Pagination.Item` is a numbered target that changes the page in this\n * component. `Pagination.Link` is the same target as a real link, for a pager\n * whose pages have addresses and can be opened in another tab. Pass\n * `getPageHref` and the whole run is drawn from links, so each page can be\n * shared, followed without JavaScript, and opened beside the one you are on.\n *\n * ## Keyboard and assistive technology\n *\n * The root is a `nav` with a label, the targets are a list, the current page\n * carries `aria-current=\"page\"`, and every arrow and gap has a label that says\n * where it goes. Every target is a real button or link, so Tab, Enter and\n * Space work as the platform defines them, without a key handler of ours. A\n * control that is dead at an end is\n * announced as disabled and stays in place, so the row does not rearrange\n * under the pointer.\n */\nimport { Children, createContext, useCallback, useContext, useMemo, useState, type ComponentProps, type ReactNode } from 'react';\nimport { tv } from 'tailwind-variants';\nimport { cn } from '@/lib/utils';\nimport {\n  clampPage,\n  paginationRange,\n  paginationSpan,\n  type PaginationItemValue,\n} from './pagination-range';\n\nexport type PaginationVariant = 'numbers' | 'compact' | 'simple';\nexport type PaginationSize = 'sm' | 'default';\n\nconst paginationVariants = tv({\n  slots: {\n    root: [\n      'group/pagination flex w-full max-w-full flex-wrap items-center gap-3',\n      'data-disabled:opacity-60',\n    ],\n    // Scrolls rather than wraps, so a run that is too wide for its column stays\n    // one row. The ring is drawn inside each target, so the clip takes nothing\n    // off it, and a keyboard focus still scrolls the target into view.\n    list: 'flex min-w-0 max-w-full items-center overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',\n    item: [\n      'relative inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap px-2 font-medium tabular-nums outline-none select-none',\n      'text-foreground transition-[color,background-color] duration-150 motion-reduce:transition-none',\n      'hover:bg-accent hover:text-accent-foreground',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n      // A spent arrow or a disabled row keeps its place and says why it does\n      // nothing, rather than vanishing and moving its neighbours.\n      'aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:bg-transparent aria-disabled:hover:text-foreground',\n      '[&_svg]:pointer-events-none [&_svg]:shrink-0',\n    ],\n    ellipsis: [\n      'relative inline-flex shrink-0 cursor-pointer items-center justify-center text-muted-foreground outline-none select-none',\n      'transition-colors duration-150 hover:bg-accent hover:text-foreground motion-reduce:transition-none',\n      'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n      'aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:bg-transparent aria-disabled:hover:text-muted-foreground',\n    ],\n    status: 'min-w-0 text-muted-foreground tabular-nums',\n    summary: 'shrink-0 text-center text-muted-foreground tabular-nums',\n    icon: 'shrink-0',\n  },\n  variants: {\n    size: {\n      default: {\n        list: 'gap-1',\n        item: 'h-9 min-w-9 rounded-lg text-sm',\n        ellipsis: 'size-9 rounded-lg',\n        status: 'text-sm',\n        summary: 'px-2 text-sm',\n        icon: 'size-4',\n      },\n      sm: {\n        list: 'gap-0.5',\n        item: 'h-8 min-w-8 rounded-md px-1.5 text-xs',\n        ellipsis: 'size-8 rounded-md',\n        status: 'text-xs',\n        summary: 'px-1.5 text-xs',\n        icon: 'size-3.5',\n      },\n    },\n    /** The page you are on, in the weight the collection uses for a chosen item. */\n    current: {\n      true: {\n        item:\n          'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 hover:text-primary-foreground',\n      },\n    },\n  },\n  defaultVariants: { size: 'default' },\n});\n\ninterface PaginationContextValue {\n  /** The page being drawn, already clamped into the set. */\n  page: number;\n  /** How many pages there are. */\n  count: number;\n  /** The target size every part reads. */\n  size: PaginationSize;\n  /** Whether the whole row is deaf. */\n  disabled: boolean;\n  /** Ask for another page. Clamped here, so a part never has to. */\n  goTo: (page: number) => void;\n}\n\nconst PaginationContext = createContext<PaginationContextValue | null>(null);\n\nfunction usePagination(part: string): PaginationContextValue {\n  const context = useContext(PaginationContext);\n  if (!context) throw new Error(`${part} must be used inside <Pagination>.`);\n  return context;\n}\n\nexport interface PaginationProps extends Omit<ComponentProps<'nav'>, 'children'> {\n  /** How many pages there are. Pages are numbered from 1, so this is also the last page's number. */\n  count: number;\n  /** The page being shown. Pass it to control the component. */\n  page?: number;\n  /** The page to start on when the component keeps its own. Defaults to 1. */\n  defaultPage?: number;\n  /** Called with the page that was asked for, already clamped to `count`. */\n  onPageChange?: (page: number) => void;\n  /** Which presentation to draw. Default `numbers`. */\n  variant?: PaginationVariant;\n  /** Its height and text size. Default `default`. */\n  size?: PaginationSize;\n  /** How many pages to keep either side of the current one. Default 1. */\n  siblings?: number;\n  /** How many pages to keep pinned at each end of the run. Default 1. */\n  boundaries?: number;\n  /**\n   * Show the previous and next arrows. Turning them off leaves the numbers\n   * alone, for a row that something else moves — a swipe, a scroller reaching\n   * its end.\n   */\n  controls?: boolean;\n  /** How far pressing an ellipsis jumps. Default 5. */\n  pageJump?: number;\n  /**\n   * Turns a page number into its address. When set, every numbered target is\n   * a real link — one that opens in another tab and works without JavaScript —\n   * instead of a button that only moves this component.\n   */\n  getPageHref?: (page: number) => string;\n  /** Greys out and deafens the whole row, for a page that is still loading. */\n  disabled?: boolean;\n  /** Labels the row for a screen reader. Default \"Pagination\". */\n  'aria-label'?: string;\n  /** Leading content, where a `Pagination.Status` or a page-size control goes. */\n  children?: ReactNode;\n  /** Extra classes for the row. */\n  className?: string;\n}\n\n/**\n * Holds the page and draws the row. Give every pagination on a page its own\n * `aria-label` when there is more than one, so a screen reader can tell them\n * apart.\n */\nfunction PaginationRoot({\n  count,\n  page: controlledPage,\n  defaultPage = 1,\n  onPageChange,\n  variant = 'numbers',\n  size = 'default',\n  siblings = 1,\n  boundaries = 1,\n  controls = true,\n  pageJump = 5,\n  getPageHref,\n  disabled = false,\n  children,\n  className,\n  'aria-label': ariaLabel = 'Pagination',\n  ...props\n}: PaginationProps) {\n  const [uncontrolledPage, setUncontrolledPage] = useState(() => clampPage(defaultPage, count));\n  const isControlled = controlledPage !== undefined;\n\n  // Clamped on the way out as well as on the way in: `count` can shrink under\n  // a controlled page — a filter narrowing the result set — and a page number\n  // past the end would light nothing and disable both arrows at once.\n  const current = clampPage(isControlled ? controlledPage : uncontrolledPage, count);\n\n  const goTo = useCallback(\n    (next: number) => {\n      const target = clampPage(next, count);\n      if (disabled || target === 0 || target === current) return;\n      if (!isControlled) setUncontrolledPage(target);\n      onPageChange?.(target);\n    },\n    [count, current, disabled, isControlled, onPageChange]\n  );\n\n  const context = useMemo<PaginationContextValue>(\n    () => ({ page: current, count, size, disabled, goTo }),\n    [current, count, size, disabled, goTo]\n  );\n\n  const items = useMemo<readonly PaginationItemValue[]>(\n    () =>\n      variant === 'numbers'\n        ? paginationRange({ count, page: current, siblings, boundaries })\n        : [],\n    [variant, count, current, siblings, boundaries]\n  );\n\n  const slots = paginationVariants({ size });\n  const hasChildren = Children.toArray(children).length > 0;\n\n  return (\n    <PaginationContext.Provider value={context}>\n      <nav\n        data-slot=\"pagination\"\n        data-variant={variant}\n        data-size={size}\n        data-disabled={disabled || undefined}\n        aria-label={ariaLabel}\n        className={slots.root({\n          className: cn(hasChildren ? 'justify-between' : 'justify-center', className),\n        })}\n        {...props}\n      >\n        {hasChildren ? (\n          <div data-slot=\"pagination-leading\" className=\"flex min-w-0 flex-wrap items-center gap-3\">\n            {typeof children === 'string' || typeof children === 'number' ? (\n              <PaginationStatus>{children}</PaginationStatus>\n            ) : (\n              children\n            )}\n          </div>\n        ) : null}\n\n        <ul data-slot=\"pagination-list\" className={slots.list()}>\n          {controls ? (\n            <li className=\"flex\">\n              <PaginationPrevious label={variant === 'simple'} />\n            </li>\n          ) : null}\n\n          {variant === 'numbers'\n            ? items.map((item, index) => (\n                <li className=\"flex\" key={typeof item === 'number' ? `page-${item}` : `${item}-${index}`}>\n                  {typeof item === 'number' ? (\n                    getPageHref ? (\n                      <PaginationLink page={item} href={getPageHref(item)} />\n                    ) : (\n                      <PaginationItem page={item} />\n                    )\n                  ) : (\n                    <PaginationEllipsis\n                      direction={item === 'start-ellipsis' ? -1 : 1}\n                      jump={pageJump}\n                    />\n                  )}\n                </li>\n              ))\n            : null}\n\n          {variant === 'compact' ? (\n            <li className=\"flex\">\n              <PaginationSummary />\n            </li>\n          ) : null}\n\n          {controls ? (\n            <li className=\"flex\">\n              <PaginationNext label={variant === 'simple'} />\n            </li>\n          ) : null}\n        </ul>\n      </nav>\n    </PaginationContext.Provider>\n  );\n}\nPaginationRoot.displayName = 'Pagination';\n\nexport interface PaginationItemProps extends Omit<ComponentProps<'button'>, 'children'> {\n  /** The page this target goes to. */\n  page: number;\n  /** What a screen reader calls this target. Defaults to \"Go to page N\". */\n  'aria-label'?: string;\n  /** Replaces the number, for a target that says something other than its page. */\n  children?: ReactNode;\n  /** Extra classes for the target. */\n  className?: string;\n}\n\n/**\n * One numbered target. It is announced as the current page when it is, so the\n * page you are on is spoken as state and not only painted.\n */\nfunction PaginationItem({\n  page,\n  children,\n  className,\n  onClick,\n  disabled: ownDisabled,\n  'aria-label': ariaLabel,\n  ...props\n}: PaginationItemProps) {\n  const { page: current, size, disabled, goTo } = usePagination('Pagination.Item');\n  const isCurrent = page === current;\n  const isDisabled = disabled || ownDisabled === true;\n  const slots = paginationVariants({ size, current: isCurrent });\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"pagination-item\"\n      data-current={isCurrent || undefined}\n      aria-current={isCurrent ? 'page' : undefined}\n      aria-label={ariaLabel ?? `Go to page ${page}`}\n      aria-disabled={isDisabled || undefined}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented || isDisabled) return;\n        goTo(page);\n      }}\n      className={slots.item({ className })}\n      {...props}\n    >\n      {children ?? page}\n    </button>\n  );\n}\nPaginationItem.displayName = 'Pagination.Item';\n\nexport interface PaginationLinkProps extends Omit<ComponentProps<'a'>, 'children' | 'href'> {\n  /** The page this link goes to. */\n  page: number;\n  /** The address of that page. A real link works without JavaScript and can be opened in another tab. */\n  href: string;\n  /** What a screen reader calls this target. Defaults to \"Go to page N\". */\n  'aria-label'?: string;\n  /** Replaces the number. */\n  children?: ReactNode;\n  /** Extra classes for the target. */\n  className?: string;\n}\n\n/**\n * A numbered target drawn as a link, for a pager whose pages have addresses.\n *\n * A modified click — a new tab, a new window — is left to the browser, so the\n * page the reader is on is not changed behind it. A plain click still moves\n * the component, so a router or a caller holding the page sees it change\n * before the navigation lands.\n */\nfunction PaginationLink({\n  page,\n  href,\n  children,\n  className,\n  onClick,\n  'aria-label': ariaLabel,\n  ...props\n}: PaginationLinkProps) {\n  const { page: current, size, disabled, goTo } = usePagination('Pagination.Link');\n  const isCurrent = page === current;\n  const slots = paginationVariants({ size, current: isCurrent });\n\n  return (\n    <a\n      data-slot=\"pagination-link\"\n      data-current={isCurrent || undefined}\n      href={href}\n      aria-current={isCurrent ? 'page' : undefined}\n      aria-label={ariaLabel ?? `Go to page ${page}`}\n      aria-disabled={disabled || undefined}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        if (disabled || isCurrent) {\n          event.preventDefault();\n          return;\n        }\n        if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {\n          return;\n        }\n        goTo(page);\n      }}\n      className={slots.item({ className })}\n      {...props}\n    >\n      {children ?? page}\n    </a>\n  );\n}\nPaginationLink.displayName = 'Pagination.Link';\n\nexport interface PaginationPreviousProps extends Omit<ComponentProps<'button'>, 'children'> {\n  /** Write the word beside the arrow rather than leaving it as a glyph. */\n  label?: boolean;\n  /** What a screen reader calls the control. Defaults to \"Previous page\". */\n  'aria-label'?: string;\n  /** Extra classes for the control. */\n  className?: string;\n}\n\nexport interface PaginationNextProps extends Omit<ComponentProps<'button'>, 'children'> {\n  /** Write the word beside the arrow rather than leaving it as a glyph. */\n  label?: boolean;\n  /** What a screen reader calls the control. Defaults to \"Next page\". */\n  'aria-label'?: string;\n  /** Extra classes for the control. */\n  className?: string;\n}\n\ninterface PaginationArrowProps {\n  step: -1 | 1;\n  part: string;\n  word: string;\n  label?: boolean;\n  className?: string;\n  disabled?: boolean;\n  'aria-label'?: string;\n  onClick?: ComponentProps<'button'>['onClick'];\n}\n\n/**\n * The two arrows, which are one control pointed the other way.\n *\n * A spent arrow is `aria-disabled` rather than removed from the page: it\n * keeps its place, so the row does not rearrange, and it stays in the tab\n * order, so a keyboard user can hear that there is a first page rather than\n * finding the row shorter than it looked.\n */\nfunction PaginationArrow({\n  step,\n  part,\n  word,\n  label = false,\n  className,\n  disabled: ownDisabled,\n  onClick,\n  'aria-label': ariaLabel,\n  ...props\n}: PaginationArrowProps) {\n  const { page, count, size, disabled, goTo } = usePagination(part);\n  const slots = paginationVariants({ size });\n  const target = page + step;\n  const spent = target < 1 || target > count;\n  const isDisabled = disabled || spent || ownDisabled === true;\n\n  return (\n    <button\n      type=\"button\"\n      data-slot={step === -1 ? 'pagination-previous' : 'pagination-next'}\n      aria-label={ariaLabel ?? (step === -1 ? 'Previous page' : 'Next page')}\n      aria-disabled={isDisabled || undefined}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented || isDisabled) return;\n        goTo(target);\n      }}\n      className={slots.item({\n        className: cn(label && 'gap-1.5 px-3', label && size === 'sm' && 'px-2', className),\n      })}\n      {...props}\n    >\n      {step === -1 ? <Chevron direction=\"left\" className={slots.icon()} /> : null}\n      {label ? <span>{word}</span> : null}\n      {step === 1 ? <Chevron direction=\"right\" className={slots.icon()} /> : null}\n    </button>\n  );\n}\n\n/** Back one page. Dead on the first page rather than looping to the last. */\nfunction PaginationPrevious({ label, ...props }: PaginationPreviousProps) {\n  return <PaginationArrow step={-1} part=\"Pagination.Previous\" word=\"Previous\" label={label} {...props} />;\n}\nPaginationPrevious.displayName = 'Pagination.Previous';\n\n/** Forward one page. Dead on the last page rather than looping to the first. */\nfunction PaginationNext({ label, ...props }: PaginationNextProps) {\n  return <PaginationArrow step={1} part=\"Pagination.Next\" word=\"Next\" label={label} {...props} />;\n}\nPaginationNext.displayName = 'Pagination.Next';\n\nexport interface PaginationEllipsisProps extends Omit<ComponentProps<'button'>, 'children'> {\n  /** Which way the gap runs: `-1` towards page 1, `1` towards the last page. */\n  direction?: -1 | 1;\n  /** How many pages a press covers. Default 5. */\n  jump?: number;\n  /** What a screen reader calls the control. Defaults to \"Jump back/forward N pages\". */\n  'aria-label'?: string;\n  /** Replaces the three dots. */\n  children?: ReactNode;\n  /** Extra classes for the control. */\n  className?: string;\n}\n\n/**\n * The gap in the run, and a way across it. Pressing it jumps `jump` pages\n * towards the end the gap is on, which is why the two gaps are told apart:\n * a jump that always went forwards would strand anyone reading the row right\n * to left.\n */\nfunction PaginationEllipsis({\n  direction = 1,\n  jump = 5,\n  children,\n  className,\n  onClick,\n  disabled: ownDisabled,\n  'aria-label': ariaLabel,\n  ...props\n}: PaginationEllipsisProps) {\n  const { page, size, disabled, goTo } = usePagination('Pagination.Ellipsis');\n  const slots = paginationVariants({ size });\n  const distance = Number.isFinite(jump) ? Math.max(1, Math.floor(jump)) : 5;\n  const isDisabled = disabled || ownDisabled === true;\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"pagination-ellipsis\"\n      data-direction={direction === -1 ? 'back' : 'forward'}\n      aria-label={\n        ariaLabel ?? (direction === -1 ? `Jump back ${distance} pages` : `Jump forward ${distance} pages`)\n      }\n      aria-disabled={isDisabled || undefined}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented || isDisabled) return;\n        goTo(page + direction * distance);\n      }}\n      className={slots.ellipsis({ className })}\n      {...props}\n    >\n      {children ?? <EllipsisGlyph className={slots.icon()} />}\n    </button>\n  );\n}\nPaginationEllipsis.displayName = 'Pagination.Ellipsis';\n\nexport interface PaginationSummaryProps extends Omit<ComponentProps<'span'>, 'children'> {\n  /** Extra classes for the line. */\n  className?: string;\n}\n\n/**\n * `3 / 12` — where you are, in the space two arrows leave between them.\n *\n * Digits and a slash rather than \"Page 3 of 12\", because the row has no room\n * for the sentence and the sentence would need translating. A screen reader\n * still hears the words, placed around the numbers with `sr-only`.\n */\nfunction PaginationSummary({ className, ...props }: PaginationSummaryProps) {\n  const { page, count, size } = usePagination('Pagination.Summary');\n  const slots = paginationVariants({ size });\n\n  return (\n    <span data-slot=\"pagination-summary\" className={slots.summary({ className })} {...props}>\n      <span className=\"sr-only\">Page </span>\n      {page}\n      <span aria-hidden=\"true\"> / </span>\n      <span className=\"sr-only\"> of </span>\n      {count}\n    </span>\n  );\n}\nPaginationSummary.displayName = 'Pagination.Summary';\n\nexport interface PaginationStatusProps extends Omit<ComponentProps<'span'>, 'children'> {\n  /** Which page the span is counted from. Read from the root when left out. */\n  page?: number;\n  /** How many rows a page holds. Required, with `total`, for a range. */\n  pageSize?: number;\n  /** How many rows there are altogether. */\n  total?: number;\n  /** Words to show instead of a worked-out span, for a set whose size is not known yet. */\n  children?: ReactNode;\n  /** Extra classes for the line. */\n  className?: string;\n}\n\n/**\n * The line that says how much of the set you are looking at: `1–20 of 143`.\n *\n * It answers the question a page number alone does not, which is how much is\n * left. Given `pageSize` and `total` it draws a range; inside a `Pagination`\n * without them it falls back to `3 / 12`; given children it shows those\n * instead.\n */\nfunction PaginationStatus({\n  page: ownPage,\n  pageSize,\n  total,\n  children,\n  className,\n  ...props\n}: PaginationStatusProps) {\n  const context = useContext(PaginationContext);\n  const size = context?.size ?? 'default';\n  const page = ownPage ?? context?.page ?? 1;\n  const count = context?.count;\n  const slots = paginationVariants({ size });\n\n  const span = useMemo(() => {\n    if (children !== undefined) return null;\n    if (pageSize !== undefined && total !== undefined) return paginationSpan({ page, pageSize, total });\n    if (count !== undefined) return `${page} / ${count}`;\n    return null;\n  }, [children, page, pageSize, total, count]);\n\n  return (\n    <span data-slot=\"pagination-status\" className={slots.status({ className })} {...props}>\n      {span ?? children}\n    </span>\n  );\n}\nPaginationStatus.displayName = 'Pagination.Status';\n\nfunction Chevron({ direction, className }: { direction: 'left' | 'right'; className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      aria-hidden=\"true\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn('rtl:-scale-x-100', className)}\n    >\n      <path d={direction === 'left' ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6'} />\n    </svg>\n  );\n}\n\nfunction EllipsisGlyph({ className }: { className?: string }) {\n  return (\n    <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" fill=\"currentColor\" className={className}>\n      <circle cx=\"5\" cy=\"12\" r=\"1.6\" />\n      <circle cx=\"12\" cy=\"12\" r=\"1.6\" />\n      <circle cx=\"19\" cy=\"12\" r=\"1.6\" />\n    </svg>\n  );\n}\n\nexport const Pagination = Object.assign(PaginationRoot, {\n  Previous: PaginationPrevious,\n  Next: PaginationNext,\n  Item: PaginationItem,\n  Link: PaginationLink,\n  Ellipsis: PaginationEllipsis,\n  Status: PaginationStatus,\n  Summary: PaginationSummary,\n});\n\nexport {\n  PaginationPrevious,\n  PaginationNext,\n  PaginationItem,\n  PaginationLink,\n  PaginationEllipsis,\n  PaginationStatus,\n  PaginationSummary,\n};\n\nexport { clampPage, paginationRange, paginationSpan, type PaginationItemValue };\n"
    },
    {
      "path": "components/panelui/pagination/pagination-range.ts",
      "type": "registry:component",
      "target": "@components/panelui/pagination/pagination-range.ts",
      "content": "/**\n * The arithmetic behind Pagination, kept away from the drawing.\n *\n * A pager is two separate problems: deciding which numbers to show, and\n * putting them on the screen. The first is the one that goes wrong quietly —\n * an ellipsis that hides a single page, a run that changes width as you step\n * through it, a page number left past the end when a filter shrinks the\n * result set. Keeping it here means it can be tested without a browser, and\n * a caller who needs the same run for a control of their own gets the same\n * answer rather than a second implementation that drifts.\n *\n * The run is a fixed width rather than a sliding window: the same number of\n * targets whichever page you are on, so the row does not reflow under the\n * finger and the arrow somebody was aiming at stays put. `boundaries` pins\n * the ends, `siblings` pads the middle, and a gap of exactly one page is\n * drawn as that page instead of an ellipsis — a gap marker wider than the\n * number it hides is a worse trade than the number.\n */\n\n/** A number in the run, or the gap where the numbers between are folded. */\nexport type PaginationItemValue = number | 'start-ellipsis' | 'end-ellipsis';\n\nconst range = (start: number, end: number): number[] =>\n  end < start ? [] : Array.from({ length: end - start + 1 }, (_, index) => start + index);\n\n/** A count of pages: a whole number, never below zero. */\nconst whole = (value: number, fallback: number): number =>\n  Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;\n\n/**\n * A page number the component can use: whole, at least 1, and never past the\n * last page.\n *\n * Both ends matter. A controlled page can be set to 0 by a stepper, and\n * `count` can shrink under a page that was valid a moment ago when a filter\n * narrows the result set. A page past the end would light nothing and disable\n * both arrows at once, which reads as a pager that has stopped working.\n *\n * A set with no pages has no page to be on, so this returns 0; the component\n * draws an empty run rather than a phantom page 1.\n */\nexport function clampPage(page: number, count: number): number {\n  const pages = whole(count, 0);\n  if (pages <= 0) return 0;\n  if (!Number.isFinite(page)) return 1;\n  return Math.min(Math.max(Math.floor(page), 1), pages);\n}\n\n/**\n * Which numbers to draw, and where the gaps go.\n *\n * `page` is clamped first, so a caller may pass the page it is holding rather\n * than the page the component is drawing. A gap of exactly one page is\n * written as the page itself: an ellipsis there would take the same width and\n * hide a target for nothing.\n */\nexport function paginationRange({\n  count,\n  page,\n  siblings = 1,\n  boundaries = 1,\n}: {\n  /** How many pages there are, numbered from 1. */\n  count: number;\n  /** The page being shown. Clamped into the run. */\n  page: number;\n  /** How many pages to keep either side of the current one. */\n  siblings?: number;\n  /** How many pages to keep pinned at each end. */\n  boundaries?: number;\n}): PaginationItemValue[] {\n  const pages = whole(count, 0);\n  if (pages === 0) return [];\n\n  const current = clampPage(page, pages);\n  const edge = whole(boundaries, 1);\n  const around = whole(siblings, 1);\n\n  const startPages = range(1, Math.min(edge, pages));\n  const endPages = range(Math.max(pages - edge + 1, edge + 1), pages);\n\n  const siblingsStart = Math.max(\n    Math.min(current - around, pages - edge - around * 2 - 1),\n    edge + 2\n  );\n  const siblingsEnd = Math.min(\n    Math.max(current + around, edge + around * 2 + 2),\n    endPages.length > 0 ? (endPages[0] as number) - 2 : pages - 1\n  );\n\n  return [\n    ...startPages,\n    ...(siblingsStart > edge + 2\n      ? (['start-ellipsis'] as PaginationItemValue[])\n      : edge + 1 < pages - edge\n        ? [edge + 1]\n        : []),\n    ...range(siblingsStart, siblingsEnd),\n    ...(siblingsEnd < pages - edge - 1\n      ? (['end-ellipsis'] as PaginationItemValue[])\n      : pages - edge > edge\n        ? [pages - edge]\n        : []),\n    ...endPages,\n  ];\n}\n\n/**\n * The rows on the current page, as a sentence: `1–20 of 143`.\n *\n * The last page is nearly always short, so the end of the span is cut at the\n * total rather than counted as a full page past it — the classic off-by-a-page\n * -size that makes a status line claim rows that do not exist.\n *\n * Needs both `pageSize` and `total` to answer. Without them it returns `null`\n * so the caller can fall back to a page count or its own words. A set with no\n * rows says so in words rather than in a range from zero to zero.\n */\nexport function paginationSpan({\n  page,\n  pageSize,\n  total,\n}: {\n  /** The page being shown. Clamped into the set. */\n  page: number;\n  /** How many rows a page holds. */\n  pageSize: number;\n  /** How many rows there are altogether. */\n  total: number;\n}): string | null {\n  if (!Number.isFinite(total) || total <= 0) return 'No results';\n\n  const size = whole(pageSize, 0);\n  if (size === 0) return null;\n\n  const rows = Math.floor(total);\n  const lastPage = Math.max(1, Math.ceil(rows / size));\n  const current = Math.min(Math.max(Number.isFinite(page) ? Math.floor(page) : 1, 1), lastPage);\n  const from = (current - 1) * size + 1;\n  const to = Math.min(current * size, rows);\n\n  return `${from}–${to} of ${rows}`;\n}\n"
    }
  ]
}
