{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pagination",
  "type": "registry:component",
  "title": "Pagination",
  "description": "Moves through paginated content. Square controls; the current page fills with ink.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/vlak-lib.json",
    "https://vlak.dev/r/icons.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/pagination.tsx",
      "content": "import * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { Icon } from \"./icons\";\n\nexport interface PaginationProps extends React.HTMLAttributes<HTMLElement> {\n  /** Current 1-based page. */\n  page: number;\n  count: number;\n  onPageChange?: (page: number) => void;\n  /** Pages kept visible around the current one. */\n  siblings?: number;\n}\n\nconst styles = stylex.create({\n  pages: {\n    display: \"flex\",\n    alignItems: \"center\",\n    gap: {\n      default: \"0.3125rem\",\n      [mq.phone]: \"0.5rem\",\n    },\n    flexWrap: {\n      default: null,\n      [mq.phone]: \"wrap\",\n    },\n  },\n  page: {\n    transition: {\n      default: vlak.transition,\n      [mq.reduce]: \"none\",\n    },\n    boxSizing: \"border-box\",\n    width: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    height: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    minWidth: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    minHeight: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    display: \"flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: vlak.controlFs,\n    },\n    color: {\n      default: vlak.gray,\n      [mq.forcedColors]: {\n        default: \"ButtonText\",\n        \":disabled\": \"GrayText\",\n      },\n    },\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    borderColor: {\n      default: vlak.controlBorder,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"ButtonText\" } },\n      [mq.forcedColors]: {\n        default: \"ButtonText\",\n        \":disabled\": \"GrayText\",\n      },\n    },\n    padding: 0,\n    backgroundColor: {\n      default: \"transparent\",\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"ButtonFace\" } },\n      [mq.forcedColors]: \"ButtonFace\",\n    },\n    fontFamily: \"inherit\",\n    cursor: {\n      default: \"pointer\",\n      \":disabled\": \"not-allowed\",\n    },\n    opacity: {\n      default: 1,\n      \":disabled\": 0.4,\n      [mq.forcedColors]: {\n        default: 1,\n        \":disabled\": 1,\n      },\n    },\n    outlineWidth: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n    outlineStyle: {\n      default: null,\n      \":focus-visible\": \"solid\",\n    },\n    outlineColor: {\n      default: null,\n      \":focus-visible\": vlak.ink,\n    },\n    outlineOffset: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n  },\n  on: {\n    backgroundColor: {\n      default: vlak.ink,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.ink, [mq.forcedColors]: \"Highlight\" } },\n      [mq.forcedColors]: \"Highlight\",\n    },\n    color: {\n      default: vlak.paper,\n      [mq.forcedColors]: \"HighlightText\",\n    },\n    fontWeight: 600,\n    borderColor: {\n      default: \"transparent\",\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: \"transparent\", [mq.forcedColors]: \"Highlight\" } },\n      [mq.forcedColors]: \"Highlight\",\n    },\n    forcedColorAdjust: \"none\",\n  },\n  gap: {\n    borderColor: { default: \"transparent\", \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: \"transparent\" } },\n    backgroundColor: { default: \"transparent\", \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: \"transparent\" } },\n    cursor: \"default\",\n  },\n  icon: {\n    display: \"block\",\n    color: \"inherit\",\n  },\n});\n\nfunction pageItems(page: number, count: number, siblings: number): Array<number | \"gap\"> {\n  const wanted = new Set<number>([1, count]);\n  for (let p = page - siblings; p <= page + siblings; p++) {\n    if (p >= 1 && p <= count) wanted.add(p);\n  }\n  const sorted = [...wanted].sort((a, b) => a - b);\n  const items: Array<number | \"gap\"> = [];\n  let prev = 0;\n  for (const p of sorted) {\n    if (prev && p - prev === 2) items.push(prev + 1);\n    else if (prev && p - prev > 2) items.push(\"gap\");\n    items.push(p);\n    prev = p;\n  }\n  return items;\n}\n\nexport const Pagination = React.forwardRef<HTMLElement, PaginationProps>(function Pagination({\n  page,\n  count,\n  onPageChange,\n  siblings = 1,\n  className,\n  style,\n  ...props\n}, ref) {\n  const nav = rs([\"rs-pages\", className], styles.pages);\n  const icon = rs([\"rs-pages-icon\"], styles.icon);\n  return (\n    <nav ref={ref} aria-label=\"Pagination\" {...props} className={nav.className} style={{ ...nav.style, ...style }}>\n      <PageButton aria-label=\"Previous page\" disabled={page <= 1} onClick={() => onPageChange?.(page - 1)}>\n        <Icon name=\"chevron-left\" size={12} className={icon.className} style={icon.style} />\n      </PageButton>\n      {pageItems(page, count, siblings).map((item, index) => {\n        if (item === \"gap\") {\n          const gap = rs([\"rs-page\", \"rs-page-gap\"], styles.page, styles.gap);\n          return (\n            <span key={`gap-${index}`} className={gap.className} style={gap.style} aria-hidden=\"true\">\n              …\n            </span>\n          );\n        }\n        return (\n          <PageButton\n            key={item}\n            current={item === page}\n            aria-current={item === page ? \"page\" : undefined}\n            onClick={() => onPageChange?.(item)}\n          >\n            {item}\n          </PageButton>\n        );\n      })}\n      <PageButton aria-label=\"Next page\" disabled={page >= count} onClick={() => onPageChange?.(page + 1)}>\n        <Icon name=\"chevron-right\" size={12} className={icon.className} style={icon.style} />\n      </PageButton>\n    </nav>\n  );\n});\n\nfunction PageButton({\n  current,\n  className,\n  style,\n  ...props\n}: React.ButtonHTMLAttributes<HTMLButtonElement> & { current?: boolean }) {\n  const sx = rs([\"rs-page\", current && \"rs-page-on\", className], styles.page, current && styles.on);\n  return <button type=\"button\" {...props} className={sx.className} style={{ ...sx.style, ...style }} />;\n}\n",
      "type": "registry:component",
      "target": "components/vlak/pagination.tsx"
    },
    {
      "path": "vlak/styles/pagination.css",
      "content": "/* ── pagination: generated from packages/react/src/components/pagination.tsx ── */\n.rs-pages{display:flex;align-items:center;gap:0.3125rem}\n@media (max-width: 640px){.rs-pages{gap:0.5rem;flex-wrap:wrap}}\n.rs-page{transition:var(--transition);box-sizing:border-box;width:var(--hit);height:var(--hit);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:0.8125rem;color:var(--text-secondary);border-width:1px;border-style:solid;border-color:var(--control-border);padding:0;background-color:transparent;font-family:inherit;cursor:pointer;opacity:1}\n.rs-page:disabled{cursor:not-allowed;opacity:0.4}\n.rs-page:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n@media (prefers-reduced-motion: reduce){.rs-page{transition:none}}\n@media (max-width: 640px){.rs-page{width:var(--hit);height:var(--hit);min-width:var(--hit);min-height:var(--hit);border-radius:var(--radius-sm);font-size:var(--control-fs)}}\n@media (forced-colors: active){.rs-page{color:ButtonText;border-color:ButtonText;background-color:ButtonFace;opacity:1}.rs-page:disabled{color:GrayText;border-color:GrayText;opacity:1}}\n@media (hover: hover) and (pointer: fine){.rs-page:hover:not(:disabled):not([aria-disabled='true']){border-color:var(--control-fill);background-color:var(--control-fill)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-page:hover:not(:disabled):not([aria-disabled='true']){border-color:ButtonText;background-color:ButtonFace}}\n.rs-page-on{background-color:var(--text);color:var(--bg);font-weight:600;border-color:transparent;forced-color-adjust:none}\n@media (hover: hover) and (pointer: fine){.rs-page-on:hover:not(:disabled):not([aria-disabled='true']){background-color:var(--text);border-color:transparent}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-page-on:hover:not(:disabled):not([aria-disabled='true']){background-color:Highlight;border-color:Highlight}}\n@media (forced-colors: active){.rs-page-on{background-color:Highlight;color:HighlightText;border-color:Highlight}}\n.rs-page-gap{border-color:transparent;background-color:transparent;cursor:default}\n@media (hover: hover) and (pointer: fine){.rs-page-gap:hover:not(:disabled):not([aria-disabled='true']){border-color:transparent;background-color:transparent}}\n.rs-pages-icon{display:block;color:inherit}\n",
      "type": "registry:file",
      "target": "styles/vlak/pagination.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "navigation",
      "classes": [
        "rs-pages",
        "rs-page",
        "rs-page-on",
        "rs-page-gap",
        "rs-pages-icon"
      ],
      "snippet": "<div class=\"rs-pages\"><span class=\"rs-page\"><svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.5 3.75 L5.5 8.25 L10.5 12.75\" vector-effect=\"non-scaling-stroke\"/></svg></span><span class=\"rs-page rs-page-on\">1</span><span class=\"rs-page\">2</span><span class=\"rs-page\"><svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5.5 3.75 L10.5 8.25 L5.5 12.75\" vector-effect=\"non-scaling-stroke\"/></svg></span></div>",
      "cssOnly": false,
      "registryDependencies": [
        "vlak-lib",
        "icons"
      ],
      "aliases": [
        "Pagination",
        "Pager",
        "Page navigation"
      ],
      "example": "import { useState } from \"react\";\nimport { Pagination } from \"@noorddev/vlak-react\";\n\nconst [page, setPage] = useState(1);\n\n<Pagination page={page} count={12} onPageChange={setPage} siblings={1} />",
      "usage": {
        "use": [
          "Paged lists and tables where the user needs to jump to a specific page.",
          "siblings to widen the window around the current page."
        ],
        "avoid": [
          "Feeds that load more on scroll.",
          "Fewer than three pages; a previous and next pair is enough."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves between the page buttons"
        },
        {
          "keys": "Enter, Space",
          "does": "Goes to that page"
        }
      ],
      "a11y": [
        "Renders <nav aria-label=\"Pagination\"> of native buttons; the current page carries aria-current=\"page\".",
        "Previous and next are labelled and disabled at the ends; gaps are aria-hidden."
      ]
    }
  }
}
