{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "type": "registry:ui",
  "title": "Pagination",
  "description": "Numbered pages with a sliding pill, a rolling range and a steady width.",
  "categories": [
    "navigation"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json"
  ],
  "files": [
    {
      "path": "components/ui/pagination.tsx",
      "content": "\"use client\";\nimport NumberFlow from \"@number-flow/react\";\nimport { Select } from \"@base-ui/react/select\";\nimport { AnimatePresence, LayoutGroup, animate, motion, useMotionValue, useReducedMotion, type Transition } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Check, ChevronLeft, ChevronRight, ChevronsUpDown, MoreH } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\n/* -------------------------------------------------------------------------------------------------\n * The window of page numbers, on its own\n * -----------------------------------------------------------------------------------------------*/\n\nexport type PaginationItem = number | \"start-ellipsis\" | \"end-ellipsis\";\n\nconst range = (from: number, to: number) => Array.from({ length: Math.max(0, to - from + 1) }, (_, i) => from + i);\n\n/**\n * The pages to draw for a given page. Once there are more pages than slots, the list\n * always has the same length (first, last, the current page and its siblings, and an\n * ellipsis wherever a gap is two or more pages), so the control never changes width.\n */\nexport function usePagination({ page, pageCount, siblings = 1, boundaries = 1 }: { page: number; pageCount: number; siblings?: number; boundaries?: number }): PaginationItem[] {\n  const slots = siblings * 2 + 3 + boundaries * 2;\n  if (pageCount <= slots) return range(1, pageCount);\n\n  const start = range(1, boundaries);\n  const end = range(pageCount - boundaries + 1, pageCount);\n  const from = Math.max(Math.min(page - siblings, pageCount - boundaries - siblings * 2 - 1), boundaries + 2);\n  const to = Math.min(Math.max(page + siblings, boundaries + siblings * 2 + 2), pageCount - boundaries - 1);\n\n  return [\n    ...start,\n    // A gap of exactly one page shows that page instead of an ellipsis standing in for it.\n    from > boundaries + 2 ? \"start-ellipsis\" : boundaries + 1,\n    ...range(from, to),\n    to < pageCount - boundaries - 1 ? \"end-ellipsis\" : pageCount - boundaries,\n    ...end,\n  ];\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Pagination\n * -----------------------------------------------------------------------------------------------*/\n\ntype Size = \"sm\" | \"md\";\n\nexport type PaginationProps = Omit<React.ComponentProps<\"nav\">, \"onChange\"> & {\n  /** The current page, starting at 1. Use with onPageChange to control it. */\n  page?: number;\n  defaultPage?: number;\n  onPageChange?: (page: number) => void;\n  /** Total number of items. Enables the summary and the page size select. */\n  total?: number;\n  /** Number of pages, when you don't know the item count. Ignored when total is set. */\n  pageCount?: number;\n  pageSize?: number;\n  defaultPageSize?: number;\n  onPageSizeChange?: (pageSize: number) => void;\n  /** Page sizes to offer. Pass two or more to show the select. */\n  pageSizeOptions?: number[];\n  /** Pages either side of the current one. */\n  siblings?: number;\n  /** \"full\" shows numbered pages; \"compact\" shows an editable page field between the arrows. */\n  variant?: \"full\" | \"compact\";\n  size?: Size;\n  /** The \"Showing 21–40 of 312\" line. Pass a function to word it yourself, or false to hide it. */\n  summary?: boolean | ((range: { from: number; to: number; total: number }) => React.ReactNode);\n  /** What the items are called, for the summary: \"invoices\". */\n  itemName?: string;\n  /** Render pages as real links. Clicks still call onPageChange. */\n  getPageHref?: (page: number) => string;\n  /** How far an ellipsis jumps. */\n  jump?: number;\n  disabled?: boolean;\n};\n\nexport function Pagination({\n  page: pageProp,\n  defaultPage = 1,\n  onPageChange,\n  total,\n  pageCount: pageCountProp,\n  pageSize: pageSizeProp,\n  defaultPageSize,\n  onPageSizeChange,\n  pageSizeOptions,\n  siblings = 1,\n  variant = \"full\",\n  size = \"md\",\n  summary = true,\n  itemName = \"results\",\n  getPageHref,\n  jump = 5,\n  disabled = false,\n  className,\n  ...rest\n}: PaginationProps) {\n  const [pageSize, setPageSize] = useControllableState({ value: pageSizeProp, defaultValue: defaultPageSize ?? pageSizeOptions?.[0] ?? 20, onChange: onPageSizeChange });\n  const pageCount = Math.max(1, total !== undefined ? Math.ceil(total / pageSize) : (pageCountProp ?? 1));\n  const [rawPage, setRawPage] = useControllableState({ value: pageProp, defaultValue: defaultPage, onChange: onPageChange });\n  const page = Math.min(Math.max(1, rawPage), pageCount);\n  const go = (next: number) => setRawPage(Math.min(Math.max(1, next), pageCount));\n\n  // Changing the page size keeps the first row you were looking at on screen.\n  const changePageSize = (next: number) => {\n    const first = (page - 1) * pageSize;\n    setPageSize(next);\n    setRawPage(Math.floor(first / next) + 1);\n  };\n\n  const showSummary = summary !== false && total !== undefined;\n  const showSizes = !!pageSizeOptions && pageSizeOptions.length > 1 && total !== undefined;\n  const from = total ? (page - 1) * pageSize + 1 : 0;\n  const to = total ? Math.min(total, page * pageSize) : 0;\n\n  return (\n    <nav\n      aria-label=\"Pagination\"\n      data-variant={variant}\n      data-size={size}\n      data-disabled={disabled || undefined}\n      className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2.5 text-fg\", className)}\n      {...rest}\n    >\n      {(showSummary && variant === \"full\") || showSizes ? (\n        // Where you are and how much you see sit together; the pages take the other end, and\n        // move to their own line, still flush right, when the row gets too narrow.\n        <div className=\"flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2\">\n          {showSummary && variant === \"full\" && (\n            <Summary from={from} to={to} total={total} itemName={itemName} render={typeof summary === \"function\" ? summary : undefined} />\n          )}\n          {showSizes && <PageSizeSelect value={pageSize} options={pageSizeOptions} onChange={changePageSize} size={size} disabled={disabled} />}\n        </div>\n      ) : null}\n      <div className=\"ml-auto flex\">\n        {variant === \"full\" ? (\n          <PageList page={page} pageCount={pageCount} siblings={siblings} size={size} jump={jump} disabled={disabled} getPageHref={getPageHref} onGo={go} />\n        ) : (\n          <CompactPager page={page} pageCount={pageCount} size={size} disabled={disabled} getPageHref={getPageHref} onGo={go} />\n        )}\n      </div>\n    </nav>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Summary: the range rolls instead of flashing\n * -----------------------------------------------------------------------------------------------*/\n\nfunction Summary({ from, to, total, itemName, render }: { from: number; to: number; total: number; itemName: string; render?: (r: { from: number; to: number; total: number }) => React.ReactNode }) {\n  const custom = render?.({ from, to, total });\n  const n = new Intl.NumberFormat(\"en-US\").format(total);\n  // The widest the line can get sits invisibly in the same cell, so the select beside it never\n  // shuffles sideways when the range goes from 1–20 to 301–312.\n  const widest = `Showing ${n}–${n} of ${n} ${itemName}`;\n  return (\n    <p role=\"status\" aria-live=\"polite\" aria-atomic className=\"grid min-w-0 text-[12.5px] text-fg-3 tabular\">\n      {!custom && (\n        <span aria-hidden className=\"invisible col-start-1 row-start-1 h-0 overflow-hidden whitespace-nowrap\">\n          {widest}\n        </span>\n      )}\n      <span className=\"col-start-1 row-start-1\">\n      {custom ??\n        (total === 0 ? (\n          <>No {itemName}</>\n        ) : (\n          <>\n            Showing{\" \"}\n            <span className=\"text-fg-2\">\n              <NumberFlow value={from} locales=\"en-US\" willChange />–<NumberFlow value={to} locales=\"en-US\" willChange />\n            </span>{\" \"}\n            of <span className=\"text-fg-2\">{n}</span> {itemName}\n          </>\n        ))}\n      </span>\n    </p>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Numbered pages with one pill that slides between them\n * -----------------------------------------------------------------------------------------------*/\n\nconst pressable = cn(\n  \"relative inline-flex shrink-0 select-none items-center justify-center font-medium outline-none\",\n  \"touch-manipulation [-webkit-tap-highlight-color:transparent]\",\n  \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n  \"transition-[background-color,color,scale] duration-150 ease-out active:scale-[0.94] active:duration-75\",\n  \"aria-disabled:pointer-events-none aria-disabled:text-fg-4\",\n  // 44px tall hit area on touch without changing the drawing.\n  \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-1.5 pointer-coarse:after:content-['']\",\n);\n\nconst cell = (size: Size) => (size === \"sm\" ? \"h-7 min-w-7 rounded-md px-1.5 text-[12px]\" : \"h-8 min-w-8 rounded-lg px-2 text-[13px]\");\n\ntype PagerProps = {\n  page: number;\n  pageCount: number;\n  size: Size;\n  disabled: boolean;\n  getPageHref?: (page: number) => string;\n  onGo: (page: number) => void;\n};\n\nfunction PageList({ page, pageCount, siblings, size, jump, disabled, getPageHref, onGo }: PagerProps & { siblings: number; jump: number }) {\n  const items = usePagination({ page, pageCount, siblings });\n  const reduce = useReducedMotion();\n  const id = useId();\n  // A page chosen from the keyboard renders on the same frame; a click glides.\n  const [instant, setInstant] = useState(false);\n  const go = (next: number, e: React.MouseEvent) => {\n    setInstant(e.detail === 0);\n    onGo(next);\n  };\n\n  const slide: Transition = instant ? { duration: 0 } : reduce ? { duration: 0.15, ease: ease.out } : spring.snappy;\n\n  return (\n    <div className={cn(\"flex items-center\", size === \"sm\" ? \"gap-0.5\" : \"gap-1\")}>\n      <Arrow dir=\"prev\" page={page} pageCount={pageCount} size={size} disabled={disabled} getPageHref={getPageHref} onGo={go} />\n      <LayoutGroup id={id}>\n        <ul className={cn(\"relative flex items-center\", size === \"sm\" ? \"gap-0.5\" : \"gap-1\")}>\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            {items.map((item) => (\n              <motion.li\n                key={item}\n                layout={reduce || instant ? false : \"position\"}\n                initial={{ opacity: 0, scale: reduce ? 1 : 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: reduce ? 1 : 0.8, transition: { duration: instant ? 0 : 0.1 } }}\n                transition={slide}\n                className=\"flex\"\n              >\n                {typeof item === \"number\" ? (\n                  <PageButton value={item} current={item === page} size={size} disabled={disabled} href={getPageHref?.(item)} pillId={`${id}-pill`} slide={slide} onGo={go} />\n                ) : (\n                  <Ellipsis\n                    dir={item === \"start-ellipsis\" ? -1 : 1}\n                    target={Math.min(Math.max(1, page + (item === \"start-ellipsis\" ? -jump : jump)), pageCount)}\n                    jump={jump}\n                    size={size}\n                    disabled={disabled}\n                    onGo={go}\n                  />\n                )}\n              </motion.li>\n            ))}\n          </AnimatePresence>\n        </ul>\n      </LayoutGroup>\n      <Arrow dir=\"next\" page={page} pageCount={pageCount} size={size} disabled={disabled} getPageHref={getPageHref} onGo={go} />\n    </div>\n  );\n}\n\ntype Go = (page: number, e: React.MouseEvent) => void;\n\nfunction PageButton({ value, current, size, disabled, href, pillId, slide, onGo }: { value: number; current: boolean; size: Size; disabled: boolean; href?: string; pillId: string; slide: Transition; onGo: Go }) {\n  const Tag = href && !disabled ? \"a\" : \"button\";\n  return (\n    <Tag\n      {...(Tag === \"a\" ? { href } : { type: \"button\" as const })}\n      aria-current={current ? \"page\" : undefined}\n      aria-label={`Page ${value}`}\n      aria-disabled={disabled || undefined}\n      data-state={current ? \"active\" : \"inactive\"}\n      onClick={(e: React.MouseEvent) => {\n        if (disabled || current) return;\n        onGo(value, e);\n      }}\n      className={cn(pressable, cell(size), \"tabular\", current ? \"text-fg\" : \"text-fg-2 hover:bg-hover hover:text-fg\")}\n    >\n      {current && (\n        <motion.span\n          layoutId={pillId}\n          transition={slide}\n          aria-hidden\n          className={cn(\"absolute inset-0 border border-line-2 bg-raised shadow-[var(--shadow)]\", size === \"sm\" ? \"rounded-md\" : \"rounded-lg\")}\n        />\n      )}\n      <span className=\"relative\">{value}</span>\n    </Tag>\n  );\n}\n\n// An ellipsis is also a shortcut: it jumps a few pages in its direction, and says so on hover and focus.\nfunction Ellipsis({ dir, target, jump, size, disabled, onGo }: { dir: -1 | 1; target: number; jump: number; size: Size; disabled: boolean; onGo: Go }) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={dir < 0 ? `Back ${jump} pages, to page ${target}` : `Forward ${jump} pages, to page ${target}`}\n      aria-disabled={disabled || undefined}\n      onClick={(e) => !disabled && onGo(target, e)}\n      className={cn(pressable, cell(size), \"group/jump text-fg-3 hover:bg-hover hover:text-fg\")}\n    >\n      <span className=\"relative grid size-4 place-items-center\">\n        <MoreH\n          size={16}\n          className=\"absolute transition-[opacity,scale] duration-150 ease-out group-hover/jump:scale-75 group-hover/jump:opacity-0 group-focus-visible/jump:scale-75 group-focus-visible/jump:opacity-0\"\n        />\n        <svg\n          width={16}\n          height={16}\n          viewBox=\"0 0 16 16\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={1.4}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          aria-hidden\n          className={cn(\n            \"absolute scale-75 opacity-0 transition-[opacity,scale] duration-150 ease-out\",\n            \"group-hover/jump:scale-100 group-hover/jump:opacity-100 group-focus-visible/jump:scale-100 group-focus-visible/jump:opacity-100\",\n          )}\n        >\n          <path d={dir < 0 ? \"M12 4.5 8.5 8l3.5 3.5M7.5 4.5 4 8l3.5 3.5\" : \"m4 4.5 3.5 3.5L4 11.5M8.5 4.5 12 8l-3.5 3.5\"} />\n        </svg>\n      </span>\n    </button>\n  );\n}\n\nfunction Arrow({ dir, page, pageCount, size, disabled, getPageHref, onGo }: Omit<PagerProps, \"onGo\"> & { dir: \"prev\" | \"next\"; onGo: Go }) {\n  const target = dir === \"prev\" ? page - 1 : page + 1;\n  // aria-disabled rather than disabled: at the first or last page the button keeps focus\n  // instead of dropping it to the body under the keyboard user's feet.\n  const off = disabled || target < 1 || target > pageCount;\n  const href = !off ? getPageHref?.(target) : undefined;\n  const Tag = href ? \"a\" : \"button\";\n  const Icon = dir === \"prev\" ? ChevronLeft : ChevronRight;\n  return (\n    <Tag\n      {...(Tag === \"a\" ? { href, rel: dir } : { type: \"button\" as const })}\n      aria-label={dir === \"prev\" ? \"Previous page\" : \"Next page\"}\n      aria-disabled={off || undefined}\n      onClick={(e: React.MouseEvent) => {\n        if (off) return e.preventDefault();\n        onGo(target, e);\n      }}\n      className={cn(pressable, cell(size), \"group/arrow px-0 text-fg-2 hover:bg-hover hover:text-fg aria-disabled:pointer-events-auto aria-disabled:cursor-not-allowed aria-disabled:active:scale-100 aria-disabled:hover:bg-transparent\")}\n    >\n      <Icon\n        size={size === \"sm\" ? 14 : 16}\n        className={cn(\n          \"transition-transform duration-150 ease-out motion-reduce:transition-none group-aria-disabled/arrow:translate-x-0\",\n          dir === \"prev\" ? \"group-hover/arrow:-translate-x-px\" : \"group-hover/arrow:translate-x-px\",\n        )}\n      />\n    </Tag>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Compact: arrows around an editable page field\n * -----------------------------------------------------------------------------------------------*/\n\nfunction CompactPager({ page, pageCount, size, disabled, getPageHref, onGo }: PagerProps) {\n  const [draft, setDraft] = useState<string | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const digits = String(pageCount).length;\n\n  const commit = () => {\n    if (draft === null) return;\n    const n = Number.parseInt(draft, 10);\n    // Out of range clamps to the nearest page; nonsense puts the old page back.\n    if (Number.isFinite(n)) onGo(Math.min(Math.max(1, n), pageCount));\n    setDraft(null);\n  };\n\n  return (\n    <div className={cn(\"flex items-center\", size === \"sm\" ? \"gap-1\" : \"gap-1.5\")}>\n      <Arrow dir=\"prev\" page={page} pageCount={pageCount} size={size} disabled={disabled} getPageHref={getPageHref} onGo={(p) => onGo(p)} />\n      <label className={cn(\"flex items-center gap-1.5 text-fg-3 tabular\", size === \"sm\" ? \"text-[12px]\" : \"text-[12.5px]\")}>\n        <span>Page</span>\n        <input\n          ref={inputRef}\n          value={draft ?? String(page)}\n          disabled={disabled}\n          inputMode=\"numeric\"\n          enterKeyHint=\"go\"\n          autoComplete=\"off\"\n          aria-label={`Page, of ${pageCount}`}\n          onFocus={(e) => e.currentTarget.select()}\n          onChange={(e) => setDraft(e.target.value.replace(/\\D/g, \"\").slice(0, digits))}\n          onBlur={commit}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              commit();\n              inputRef.current?.select();\n            } else if (e.key === \"Escape\" && draft !== null) {\n              e.preventDefault();\n              setDraft(null);\n            } else if (e.key === \"ArrowUp\" || e.key === \"ArrowDown\") {\n              e.preventDefault();\n              setDraft(null);\n              onGo(Math.min(Math.max(1, page + (e.key === \"ArrowUp\" ? 1 : -1)), pageCount));\n            }\n          }}\n          style={{ width: `calc(${Math.max(2, digits)}ch + ${size === \"sm\" ? 14 : 18}px)` }}\n          className={cn(\n            \"rounded-md border border-line-2 bg-raised text-center font-medium text-fg shadow-[var(--shadow)] tabular outline-none\",\n            \"transition-[border-color,box-shadow] duration-150 ease-out hover:border-fg-4 focus:border-fg-4 focus:ring-3 focus:ring-fg/10\",\n            \"disabled:opacity-50\",\n            // 16px on touch so the phone doesn't zoom in on focus.\n            size === \"sm\" ? \"h-7 text-base sm:text-[12px]\" : \"h-8 text-base sm:text-[13px]\",\n          )}\n        />\n        <span className=\"whitespace-nowrap\">\n          of <span className=\"text-fg-2\">{new Intl.NumberFormat(\"en-US\").format(pageCount)}</span>\n        </span>\n      </label>\n      <Arrow dir=\"next\" page={page} pageCount={pageCount} size={size} disabled={disabled} getPageHref={getPageHref} onGo={(p) => onGo(p)} />\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Page size\n * -----------------------------------------------------------------------------------------------*/\n\nfunction PageSizeSelect({ value, options, onChange, size, disabled }: { value: number; options: number[]; onChange: (n: number) => void; size: Size; disabled: boolean }) {\n  const items = options.map((n) => ({ value: n, label: `${n} per page` }));\n  return (\n    <Select.Root items={items} value={value} onValueChange={(v) => v !== null && onChange(v as number)} disabled={disabled}>\n      <Select.Trigger\n        aria-label=\"Items per page\"\n        className={cn(\n          \"group/size relative inline-flex shrink-0 select-none items-center border border-line-2 bg-raised text-fg-2 shadow-[var(--shadow)] outline-none tabular\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"transition-[background-color,border-color,color,scale] duration-150 ease-out active:scale-[0.97] active:duration-75\",\n          \"hover:border-fg-4 hover:bg-hover hover:text-fg data-popup-open:border-fg-4 data-popup-open:bg-hover data-popup-open:text-fg\",\n          \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n          \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-1.5 pointer-coarse:after:content-['']\",\n          size === \"sm\" ? \"h-7 gap-1 rounded-md pr-1 pl-2 text-[12px]\" : \"h-8 gap-1.5 rounded-lg pr-1.5 pl-2.5 text-[12.5px]\",\n        )}\n      >\n        <Select.Value />\n        <Select.Icon className=\"flex text-fg-4 transition-colors duration-150 group-hover/size:text-fg-3\">\n          <ChevronsUpDown size={14} />\n        </Select.Icon>\n      </Select.Trigger>\n      <Select.Portal>\n        <Select.Positioner sideOffset={6} collisionPadding={8} className=\"z-(--z-popover) outline-none select-none\">\n          <Select.Popup\n            className={cn(\n              \"relative min-w-(--anchor-width) origin-(--transform-origin) overflow-hidden rounded-xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n              \"transition-[opacity,scale] duration-160 ease-out-expo data-ending-style:duration-100 data-ending-style:ease-out\",\n              \"data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.98] data-ending-style:opacity-0\",\n              // Laid over the trigger, Base UI lines the chosen row up with the value; a scale would skew that, so it only fades.\n              \"data-[side=none]:data-starting-style:scale-100 data-[side=none]:data-ending-style:scale-100 data-[side=none]:min-w-[calc(var(--anchor-width)+0.5rem)]\",\n              \"motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n            )}\n          >\n            <GlideList>\n              {items.map((item) => (\n                <Select.Item\n                  key={item.value}\n                  value={item.value}\n                  className=\"relative z-[1] flex h-8 cursor-default scroll-my-1 items-center gap-2 rounded-lg pr-2 pl-2.5 text-[13px] text-fg outline-none select-none tabular pointer-coarse:h-10\"\n                >\n                  <Select.ItemText className=\"flex-1\">{item.label}</Select.ItemText>\n                  <span className=\"grid size-4 place-items-center\">\n                    <Select.ItemIndicator>\n                      <Check size={14} />\n                    </Select.ItemIndicator>\n                  </span>\n                </Select.Item>\n              ))}\n            </GlideList>\n          </Select.Popup>\n        </Select.Positioner>\n      </Select.Portal>\n    </Select.Root>\n  );\n}\n\n// One highlight for the whole list: it springs after the pointer and jumps for arrow keys.\nfunction GlideList({ children }: { children: React.ReactNode }) {\n  const reduce = useReducedMotion();\n  const [list, setList] = useState<HTMLDivElement | null>(null);\n  const y = useMotionValue(0);\n  const height = useMotionValue(32);\n  const opacity = useMotionValue(0);\n\n  useEffect(() => {\n    if (!list) return;\n    let keyboard = false;\n    let shown = false;\n    const place = () => {\n      const el = list.querySelector<HTMLElement>(\"[data-highlighted]\");\n      if (!el) {\n        shown = false;\n        animate(opacity, 0, { duration: reduce ? 0 : 0.12 });\n        return;\n      }\n      if (!shown || keyboard || reduce) {\n        y.jump(el.offsetTop);\n        height.jump(el.offsetHeight);\n        animate(opacity, 1, { duration: keyboard || reduce ? 0 : 0.08 });\n      } else {\n        animate(y, el.offsetTop, spring.follow);\n        animate(height, el.offsetHeight, spring.follow);\n      }\n      shown = true;\n    };\n    const onKey = () => (keyboard = true);\n    const onPointer = () => (keyboard = false);\n    const observer = new MutationObserver(place);\n    observer.observe(list, { subtree: true, attributes: true, attributeFilter: [\"data-highlighted\"] });\n    document.addEventListener(\"keydown\", onKey, true);\n    list.addEventListener(\"pointermove\", onPointer);\n    place();\n    return () => {\n      observer.disconnect();\n      document.removeEventListener(\"keydown\", onKey, true);\n      list.removeEventListener(\"pointermove\", onPointer);\n    };\n  }, [list, reduce, y, height, opacity]);\n\n  return (\n    <Select.List ref={setList} className=\"relative max-h-(--available-height) overflow-y-auto overscroll-contain p-1 outline-none\">\n      <motion.div aria-hidden style={{ y, height, opacity }} className=\"pointer-events-none absolute inset-x-1 top-0 rounded-lg bg-fg/[0.06]\" />\n      {children}\n    </Select.List>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/pagination"
}
