{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-navigator",
  "title": "Page Navigator",
  "description": "A rail of page thumbnails for moving through a long document.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/page-navigator/page-navigator.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { FileText } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type DocumentPage = {\n  number: number\n  src?: string\n  label?: string\n}\n\nexport type PageNavigatorProps = Omit<\n  React.HTMLAttributes<HTMLElement>,\n  \"children\" | \"onChange\"\n> & {\n  pages: DocumentPage[]\n  activePage?: number\n  defaultActivePage?: number\n  onActivePageChange?: (page: number) => void\n  orientation?: \"vertical\" | \"horizontal\"\n  label?: string\n  renderImage?: (props: {\n    src: string\n    alt: string\n    className: string\n  }) => React.ReactNode\n}\n\nexport function PageNavigator({\n  pages,\n  activePage,\n  defaultActivePage,\n  onActivePageChange,\n  orientation = \"vertical\",\n  label = \"Pages\",\n  renderImage,\n  className,\n  ...rootProps\n}: PageNavigatorProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(\n    defaultActivePage ?? pages[0]?.number ?? 1\n  )\n  const current = activePage ?? uncontrolled\n  const isVertical = orientation === \"vertical\"\n\n  const listRef = React.useRef<HTMLDivElement>(null)\n\n  const select = (page: number) => {\n    if (activePage === undefined) setUncontrolled(page)\n    onActivePageChange?.(page)\n  }\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const forward = isVertical ? \"ArrowDown\" : \"ArrowRight\"\n    const backward = isVertical ? \"ArrowUp\" : \"ArrowLeft\"\n\n    const index = pages.findIndex((page) => page.number === current)\n    if (index < 0) return\n\n    let next = index\n    if (event.key === forward) next = Math.min(index + 1, pages.length - 1)\n    else if (event.key === backward) next = Math.max(index - 1, 0)\n    else if (event.key === \"Home\") next = 0\n    else if (event.key === \"End\") next = pages.length - 1\n    else return\n\n    event.preventDefault()\n    const page = pages[next]\n    if (!page) return\n\n    select(page.number)\n    listRef.current\n      ?.querySelector<HTMLElement>(`[data-page=\"${page.number}\"]`)\n      ?.focus()\n  }\n\n  return (\n    <nav\n      data-slot=\"page-navigator\"\n      aria-label={label}\n      className={cn(\n        \"border-border bg-card text-card-foreground rounded-[var(--radius)] border\",\n        className\n      )}\n      {...rootProps}\n    >\n      <div\n        ref={listRef}\n        role=\"tablist\"\n        aria-label={label}\n        aria-orientation={orientation}\n        className={cn(\n          \"flex gap-2 p-2\",\n          isVertical\n            ? \"max-h-[26rem] flex-col overflow-y-auto\"\n            : \"flex-row overflow-x-auto\"\n        )}\n        onKeyDown={onKeyDown}\n      >\n        {pages.map((page) => {\n          const isActive = page.number === current\n\n          return (\n            <button\n              key={page.number}\n              type=\"button\"\n              role=\"tab\"\n              data-page={page.number}\n              data-slot=\"page-navigator-page\"\n              aria-selected={isActive}\n              tabIndex={isActive ? 0 : -1}\n              className={cn(\n                \"focus-visible:ring-ring group shrink-0 rounded-[calc(var(--radius)-0.25rem)] p-1 focus-visible:ring-2 focus-visible:outline-none\",\n                isActive\n                  ? \"bg-muted\"\n                  : \"hover:bg-muted/60 transition-colors duration-150 motion-reduce:transition-none\"\n              )}\n              onClick={() => select(page.number)}\n            >\n              <span\n                className={cn(\n                  \"bg-background flex aspect-[3/4] w-20 items-center justify-center overflow-hidden rounded-[3px] border\",\n                  isActive ? \"border-primary\" : \"border-border\"\n                )}\n              >\n                {page.src ? (\n                  renderImage ? (\n                    renderImage({\n                      src: page.src,\n                      alt: \"\",\n                      className: \"size-full object-cover\",\n                    })\n                  ) : (\n                    // eslint-disable-next-line @next/next/no-img-element\n                    <img\n                      alt=\"\"\n                      className=\"size-full object-cover\"\n                      src={page.src}\n                    />\n                  )\n                ) : (\n                  <FileText\n                    aria-hidden=\"true\"\n                    className=\"text-muted-foreground\"\n                    size={20}\n                  />\n                )}\n              </span>\n\n              <span\n                className={cn(\n                  \"mt-1 block text-center font-[family-name:var(--font-mono),monospace] text-[0.65rem] tabular-nums\",\n                  isActive\n                    ? \"text-foreground font-bold\"\n                    : \"text-muted-foreground\"\n                )}\n              >\n                {page.label ?? page.number}\n              </span>\n            </button>\n          )\n        })}\n      </div>\n    </nav>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "document",
    "navigation",
    "pages"
  ],
  "type": "registry:ui"
}