{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdf-viewer",
  "title": "PDF Viewer",
  "description": "A PDF viewer with page virtualization, thumbnails, search, zoom, rotation, upload, and page overlay hooks.",
  "dependencies": [
    "@embedpdf/core@^2.15.0",
    "@embedpdf/engines@^2.15.0",
    "@embedpdf/models@^2.15.0",
    "@embedpdf/plugin-document-manager@^2.15.0",
    "@embedpdf/plugin-interaction-manager@^2.15.0",
    "@embedpdf/plugin-render@^2.15.0",
    "@embedpdf/plugin-rotate@^2.15.0",
    "@embedpdf/plugin-scroll@^2.15.0",
    "@embedpdf/plugin-search@^2.15.0",
    "@embedpdf/plugin-selection@^2.15.0",
    "@embedpdf/plugin-thumbnail@^2.15.0",
    "@embedpdf/plugin-tiling@^2.15.0",
    "@embedpdf/plugin-viewport@^2.15.0",
    "@embedpdf/plugin-zoom@^2.15.0",
    "pdf-lib@^1.17.1"
  ],
  "registryDependencies": [
    "@extend/document-viewer-sidebar",
    "utils",
    "@extend/pdf-editor-core",
    "button",
    "dropdown-menu",
    "input",
    "popover",
    "scroll-area",
    "select",
    "separator",
    "tooltip"
  ],
  "files": [
    {
      "path": "components/extend/pdf-viewer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { createPluginRegistration, refreshPages } from \"@embedpdf/core\"\nimport { EmbedPDF, useRegistry } from \"@embedpdf/core/react\"\nimport type {\n  PdfDocumentObject,\n  PdfEngine,\n  Rect,\n  Rotation,\n} from \"@embedpdf/models\"\nimport {\n  DocumentManagerPluginPackage,\n  useActiveDocument,\n  useDocumentManagerCapability,\n} from \"@embedpdf/plugin-document-manager/react\"\nimport {\n  GlobalPointerProvider,\n  InteractionManagerPluginPackage,\n  PagePointerProvider,\n} from \"@embedpdf/plugin-interaction-manager/react\"\nimport { RenderLayer, RenderPluginPackage } from \"@embedpdf/plugin-render/react\"\nimport { Rotate, RotatePluginPackage } from \"@embedpdf/plugin-rotate/react\"\nimport {\n  ScrollPluginPackage,\n  ScrollStrategy,\n  useScroll,\n  useScrollPlugin,\n  type PageLayout,\n  type ScrollerLayout,\n  type VirtualItem,\n} from \"@embedpdf/plugin-scroll/react\"\nimport {\n  SearchLayer,\n  SearchPluginPackage,\n  useSearch,\n} from \"@embedpdf/plugin-search/react\"\nimport {\n  CopyToClipboard,\n  SelectionPluginPackage,\n  useSelectionCapability,\n  useSelectionPlugin,\n} from \"@embedpdf/plugin-selection/react\"\nimport {\n  ThumbImg,\n  ThumbnailPluginPackage,\n  useThumbnailCapability,\n  useThumbnailPlugin,\n  type ThumbMeta,\n} from \"@embedpdf/plugin-thumbnail/react\"\nimport { TilingLayer, TilingPluginPackage } from \"@embedpdf/plugin-tiling/react\"\nimport {\n  useIsViewportGated,\n  useViewportCapability,\n  useViewportElement,\n  useViewportRef,\n  ViewportElementContext,\n  ViewportPluginPackage,\n} from \"@embedpdf/plugin-viewport/react\"\nimport { useZoom, ZoomPluginPackage } from \"@embedpdf/plugin-zoom/react\"\nimport { flushSync } from \"react-dom\"\n\nimport { loadSharedPdfEngine } from \"@/lib/pdf-thumbnail-utils\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport {\n  DocumentViewerSidebarSkeleton,\n  DocumentViewerThumbnailSidebar,\n  useElementWidth,\n  useInlineThumbnailSidebar,\n} from \"@/components/extend/document-viewer-sidebar\"\nimport { IconPlaceholder } from \"@/components/icon-placeholder\"\n\nexport type PDFViewerPageOverlayProps = {\n  pageNumber: number\n  pageWidth: number\n  pageHeight: number\n  scale: number\n  rotation: number\n}\nexport type PDFViewerHandle = {\n  scrollToPage: (pageNumber: number, options?: ScrollIntoViewOptions) => void\n  scrollToPageArea: (\n    pageNumber: number,\n    area: {\n      top: number\n      left?: number\n      width?: number\n      height?: number\n    },\n    options?: ScrollToOptions\n  ) => void\n  getViewportElement: () => HTMLDivElement | null\n}\nexport type PDFViewerScrollAreaViewportResolver = (\n  container: HTMLDivElement\n) => HTMLDivElement | null\nexport type PDFViewerProps = {\n  className?: string\n  defaultZoom?: number\n  fileName?: string\n  resolveScrollAreaViewport?: PDFViewerScrollAreaViewportResolver\n  showDownload?: boolean\n  showToolbar?: boolean\n  showRotateControls?: boolean\n  showUpload?: boolean\n  src?: string\n  toolbarActions?: React.ReactNode\n  pageClassName?: (pageNumber: number) => string | undefined\n  renderPageOverlay?: (props: PDFViewerPageOverlayProps) => React.ReactNode\n  onActivePageChange?: (pageNumber: number) => void\n  onDocumentLoadSuccess?: (numPages: number) => void\n  onPdfUpload?: (file: File) => void\n  onPagePointerDown?: (\n    event: React.PointerEvent<HTMLDivElement>,\n    pageNumber: number\n  ) => void\n  onPagePointerMove?: (\n    event: React.PointerEvent<HTMLDivElement>,\n    pageNumber: number\n  ) => void\n  onPagePointerUp?: (\n    event: React.PointerEvent<HTMLDivElement>,\n    pageNumber: number\n  ) => void\n  onPagePointerCancel?: (\n    event: React.PointerEvent<HTMLDivElement>,\n    pageNumber: number\n  ) => void\n}\nconst DEFAULT_ZOOM = 1\nconst ZOOM_OPTIONS = [0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2]\nconst PAGE_GAP = 24\nconst THUMBNAIL_PAGE_WIDTH = 92\nconst THUMBNAIL_IMAGE_PADDING = 8\nconst THUMBNAIL_WIDTH = THUMBNAIL_PAGE_WIDTH + THUMBNAIL_IMAGE_PADDING * 2\nconst THUMBNAIL_LABEL_HEIGHT = 24\nconst THUMBNAIL_GAP = 12\nconst THUMBNAIL_PANE_PADDING_Y = 16\nconst THUMBNAIL_SIDEBAR_WIDTH_CLASS = \"w-40\"\nconst THUMBNAIL_SIDEBAR_CLOSED_CLASS = \"-ml-40\"\nconst PAGE_BASE_RENDER_MAX_SCALE = 1\nconst PAGE_BASE_RENDER_DPR = 1\nconst PDF_SEARCH_DEBOUNCE_MS = 300\nconst TEXT_SELECTION_BACKGROUND = \"rgba(59, 130, 246, 0.14)\"\nconst THUMBNAIL_FOCUS_RING_CLASS =\n  \"group-focus-visible/pdf-thumbnail-sidebar:ring-2 group-focus-visible/pdf-thumbnail-sidebar:ring-ring group-focus-visible/pdf-thumbnail-sidebar:ring-offset-1 group-focus-visible/pdf-thumbnail-sidebar:ring-offset-background\"\nconst DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR =\n  '[data-slot=\"scroll-area-viewport\"]'\nfunction resolveDefaultScrollAreaViewport(container: HTMLDivElement) {\n  return container.querySelector<HTMLDivElement>(\n    DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR\n  )\n}\nconst PDFViewerScrollAreaResolverContext =\n  React.createContext<PDFViewerScrollAreaViewportResolver>(\n    resolveDefaultScrollAreaViewport\n  )\ntype PageRotationDeltas = Map<number, Rotation>\ntype ThumbnailSelectionMode = \"replace\" | \"toggle\" | \"range\"\nfunction getPageIndexRange(from: number, to: number): Set<number> {\n  const start = Math.min(from, to)\n  const end = Math.max(from, to)\n  const range = new Set<number>()\n  for (let pageIndex = start; pageIndex <= end; pageIndex += 1) {\n    range.add(pageIndex)\n  }\n  return range\n}\nfunction arePageIndexSetsEqual(left: Set<number>, right: Set<number>) {\n  if (left.size !== right.size) return false\n  for (const value of left) {\n    if (!right.has(value)) return false\n  }\n  return true\n}\nfunction normalizeRotation(rotation: number): Rotation {\n  return (((rotation % 4) + 4) % 4) as Rotation\n}\nfunction useSharedPdfEngine() {\n  const [engine, setEngine] = React.useState<PdfEngine | null>(null)\n  const [error, setError] = React.useState<Error | null>(null)\n  React.useEffect(() => {\n    let cancelled = false\n    loadSharedPdfEngine().then(\n      (loadedEngine) => {\n        if (!cancelled) setEngine(loadedEngine)\n      },\n      (loadError: Error) => {\n        if (!cancelled) setError(loadError)\n      }\n    )\n    return () => {\n      cancelled = true\n    }\n  }, [])\n  return { engine, error }\n}\nfunction rotationToDegrees(rotation: Rotation) {\n  return (rotation as number) * 90\n}\nfunction normalizeDegrees(rotation: number) {\n  return ((rotation % 360) + 360) % 360\n}\nfunction ensurePdfExtension(fileName: string) {\n  return fileName.toLowerCase().endsWith(\".pdf\") ? fileName : `${fileName}.pdf`\n}\nfunction getPdfDownloadFileName(fileName: string | undefined, src: string) {\n  if (fileName?.trim()) return ensurePdfExtension(fileName.trim())\n  const pathname = src.split(/[?#]/)[0] ?? \"\"\n  const rawName = pathname.split(\"/\").pop() || \"document.pdf\"\n  try {\n    return ensurePdfExtension(decodeURIComponent(rawName))\n  } catch {\n    return ensurePdfExtension(rawName)\n  }\n}\nfunction getRotatedPdfDownloadFileName(fileName: string) {\n  return fileName.replace(/\\.pdf$/i, \"-rotated.pdf\")\n}\nfunction downloadBlob(blob: Blob, fileName: string) {\n  const url = URL.createObjectURL(blob)\n  const anchor = document.createElement(\"a\")\n  anchor.href = url\n  anchor.download = fileName\n  anchor.rel = \"noopener\"\n  document.body.append(anchor)\n  anchor.click()\n  anchor.remove()\n  window.setTimeout(() => URL.revokeObjectURL(url), 0)\n}\nasync function downloadPdfWithPageRotations({\n  fileName,\n  pageRotationDeltas,\n  src,\n}: {\n  fileName: string\n  pageRotationDeltas: PageRotationDeltas\n  src: string\n}) {\n  const response = await fetch(src)\n  if (!response.ok) {\n    throw new Error(`Failed to download PDF (${response.status})`)\n  }\n  if (pageRotationDeltas.size === 0) {\n    downloadBlob(await response.blob(), fileName)\n    return\n  }\n  const [{ PDFDocument, degrees }, pdfBytes] = await Promise.all([\n    import(\"pdf-lib\"),\n    response.arrayBuffer(),\n  ])\n  const pdfDocument = await PDFDocument.load(pdfBytes)\n  pdfDocument.getPages().forEach((page, pageIndex) => {\n    const rotationDelta = pageRotationDeltas.get(pageIndex)\n    if (!rotationDelta) return\n    page.setRotation(\n      degrees(\n        normalizeDegrees(\n          page.getRotation().angle + rotationToDegrees(rotationDelta)\n        )\n      )\n    )\n  })\n  const nextPdfBytes = await pdfDocument.save()\n  const nextPdfBuffer = new ArrayBuffer(nextPdfBytes.byteLength)\n  new Uint8Array(nextPdfBuffer).set(nextPdfBytes)\n  downloadBlob(\n    new Blob([nextPdfBuffer], { type: \"application/pdf\" }),\n    getRotatedPdfDownloadFileName(fileName)\n  )\n}\nfunction getThumbnailMetaForPage({\n  page,\n  pageIndex,\n  rotation,\n  width,\n  imagePadding,\n  labelHeight,\n  top,\n}: {\n  page: PdfDocumentObject[\"pages\"][number]\n  pageIndex: number\n  rotation: Rotation\n  width: number\n  imagePadding: number\n  labelHeight: number\n  top: number\n}): ThumbMeta {\n  const innerWidth = Math.max(1, width - imagePadding * 2)\n  const pageWidth = rotation % 2 === 1 ? page.size.height : page.size.width\n  const pageHeight = rotation % 2 === 1 ? page.size.width : page.size.height\n  const imageHeight = Math.round(innerWidth * (pageHeight / pageWidth))\n  const wrapperHeight = imagePadding + imageHeight + imagePadding + labelHeight\n  return {\n    pageIndex,\n    width: innerWidth,\n    height: imageHeight,\n    wrapperHeight,\n    top,\n    labelHeight,\n    padding: imagePadding,\n  }\n}\nfunction buildThumbnailLayout({\n  basePageRotations,\n  pageRotationDeltas,\n  pdfDocument,\n  width,\n  gap,\n  imagePadding,\n  labelHeight,\n  paddingY,\n}: {\n  basePageRotations: Rotation[]\n  pageRotationDeltas: PageRotationDeltas\n  pdfDocument: PdfDocumentObject | null\n  width: number\n  gap: number\n  imagePadding: number\n  labelHeight: number\n  paddingY: number\n}) {\n  if (!pdfDocument) return null\n  let top = paddingY\n  const items = pdfDocument.pages.map((page, pageIndex) => {\n    const basePageRotation =\n      basePageRotations[pageIndex] ?? normalizeRotation(page.rotation)\n    const pageRotation = normalizeRotation(\n      basePageRotation + (pageRotationDeltas.get(pageIndex) ?? 0)\n    )\n    const meta = getThumbnailMetaForPage({\n      page,\n      pageIndex,\n      rotation: pageRotation,\n      width,\n      imagePadding,\n      labelHeight,\n      top,\n    })\n    top += meta.wrapperHeight + gap\n    return meta\n  })\n  return {\n    items,\n    totalHeight: items.length ? top - gap + paddingY : paddingY * 2,\n  }\n}\nfunction getVisibleThumbnailItems({\n  buffer,\n  clientHeight,\n  items,\n  scrollTop,\n}: {\n  buffer: number\n  clientHeight: number\n  items: ThumbMeta[]\n  scrollTop: number\n}) {\n  if (items.length === 0) return []\n  if (clientHeight <= 0)\n    return items.slice(0, Math.min(items.length, buffer * 2))\n  const viewportBottom = scrollTop + clientHeight\n  let start = items.findIndex(\n    (item) => item.top + item.wrapperHeight >= scrollTop\n  )\n  if (start === -1) start = items.length - 1\n  let end = start\n  while (end < items.length && items[end].top <= viewportBottom) {\n    end += 1\n  }\n  return items.slice(\n    Math.max(0, start - buffer),\n    Math.min(items.length, end + buffer)\n  )\n}\nfunction PDFViewerLoadingSkeleton({\n  sidebarOpen,\n  sidebarInline,\n}: {\n  sidebarOpen: boolean\n  sidebarInline: boolean\n}) {\n  return (\n    <div className=\"absolute inset-0 z-20 flex bg-muted/30\">\n      {sidebarOpen ? (\n        <DocumentViewerSidebarSkeleton\n          className={THUMBNAIL_SIDEBAR_WIDTH_CLASS}\n          inline={sidebarInline}\n        />\n      ) : null}\n      <div className=\"grid min-w-0 flex-1 place-items-center\">\n        <InlineSpinner className=\"size-4\" />\n      </div>\n    </div>\n  )\n}\n// Rendered while the engine or document is not ready: same frame and controls\n// as the full viewer, with document-dependent controls disabled.\nfunction PDFViewerFallbackShell({\n  className,\n  defaultZoom,\n  errorMessage = \"Unable to load the PDF preview.\",\n  showDownload,\n  showRotateControls,\n  showToolbar,\n  showUpload,\n  sidebarOpen,\n  state,\n  toolbarActions,\n  onUploadFile,\n}: {\n  className?: string\n  defaultZoom: number\n  errorMessage?: string\n  showDownload: boolean\n  showRotateControls: boolean\n  showToolbar: boolean\n  showUpload: boolean\n  sidebarOpen: boolean\n  state: \"loading\" | \"error\" | \"empty\"\n  toolbarActions?: React.ReactNode\n  onUploadFile?: (file: File) => void\n}) {\n  return (\n    <div\n      data-slot=\"pdf-viewer\"\n      className={cn(\n        \"flex h-full max-h-full min-h-0 w-full flex-col overflow-hidden bg-background\",\n        className\n      )}\n    >\n      {showToolbar ? (\n        <PDFViewerToolbar\n          activePage={1}\n          controlsDisabled\n          currentZoomLevel={defaultZoom}\n          numPages={0}\n          searchControl={\n            <ToolbarTooltip label=\"Search text\">\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label=\"Search text\"\n                disabled\n              >\n                <IconPlaceholder\n                  lucide=\"Search\"\n                  tabler=\"IconSearch\"\n                  hugeicons=\"Search01Icon\"\n                  phosphor=\"MagnifyingGlassIcon\"\n                  remixicon=\"RiSearchLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </ToolbarTooltip>\n          }\n          sidebarOpen={sidebarOpen}\n          showDownload={showDownload}\n          showRotateControls={showRotateControls}\n          showUpload={showUpload}\n          toolbarActions={toolbarActions}\n          onPageChange={() => undefined}\n          onRotate={() => undefined}\n          onToggleSidebar={() => undefined}\n          onUploadFile={onUploadFile}\n          onZoomChange={() => undefined}\n        />\n      ) : null}\n      <div className=\"relative flex min-h-0 flex-1 overflow-hidden bg-muted/30\">\n        {state === \"loading\" ? (\n          <PDFViewerLoadingSkeleton sidebarInline sidebarOpen={sidebarOpen} />\n        ) : null}\n        {state === \"error\" ? (\n          <div className=\"absolute inset-0 z-20 grid place-items-center bg-background p-6 text-sm text-muted-foreground\">\n            {errorMessage}\n          </div>\n        ) : null}\n        {state === \"empty\" ? (\n          <div className=\"absolute inset-0 z-20 grid place-items-center bg-background p-6 text-center text-sm text-muted-foreground\">\n            <div className=\"max-w-sm space-y-3\">\n              <div className=\"font-medium text-foreground\">\n                Upload a PDF to preview\n              </div>\n              <div>\n                Pass a PDF URL with the <code>src</code> prop or use the upload\n                control.\n              </div>\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </div>\n  )\n}\nfunction ToolbarTooltip({\n  label,\n  children,\n}: {\n  label: string\n  children: React.ReactNode\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={<span className=\"inline-flex\">{children}</span>}\n      ></TooltipTrigger>\n      <TooltipContent side=\"bottom\">{label}</TooltipContent>\n    </Tooltip>\n  )\n}\nfunction PDFViewerFileActionsMenu({\n  downloadDisabled,\n  isPreparingDownload = false,\n  onDownload,\n  onUploadFile,\n  showDownload = false,\n  showUpload = false,\n}: {\n  downloadDisabled?: boolean\n  isPreparingDownload?: boolean\n  onDownload?: () => void\n  onUploadFile?: (file: File) => void\n  showDownload?: boolean\n  showUpload?: boolean\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  if (!showDownload && !showUpload) return null\n  return (\n    <>\n      {showUpload && onUploadFile ? (\n        <input\n          ref={inputRef}\n          type=\"file\"\n          accept=\"application/pdf,.pdf\"\n          className=\"sr-only\"\n          tabIndex={-1}\n          onChange={(event) => {\n            const nextFile = event.target.files?.[0]\n            if (nextFile) {\n              onUploadFile(nextFile)\n              event.currentTarget.value = \"\"\n            }\n          }}\n        />\n      ) : null}\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          render={\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label=\"Open PDF actions\"\n            >\n              <IconPlaceholder\n                lucide=\"Ellipsis\"\n                tabler=\"IconDots\"\n                hugeicons=\"MoreHorizontalIcon\"\n                phosphor=\"DotsThreeIcon\"\n                remixicon=\"RiMoreLine\"\n                className=\"size-4\"\n              />\n            </Button>\n          }\n        ></DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\" className=\"w-40\">\n          {showDownload ? (\n            <DropdownMenuItem disabled={downloadDisabled} onClick={onDownload}>\n              {isPreparingDownload ? (\n                <InlineSpinner className=\"size-4\" />\n              ) : (\n                <IconPlaceholder\n                  lucide=\"Download\"\n                  tabler=\"IconDownload\"\n                  hugeicons=\"Download01Icon\"\n                  phosphor=\"DownloadSimpleIcon\"\n                  remixicon=\"RiDownload2Line\"\n                  className=\"size-4\"\n                />\n              )}\n              Download\n            </DropdownMenuItem>\n          ) : null}\n          {showUpload && onUploadFile ? (\n            <DropdownMenuItem onClick={() => inputRef.current?.click()}>\n              <IconPlaceholder\n                lucide=\"Upload\"\n                tabler=\"IconUpload\"\n                hugeicons=\"Upload01Icon\"\n                phosphor=\"UploadSimpleIcon\"\n                remixicon=\"RiUpload2Line\"\n                className=\"size-4\"\n              />\n              Upload\n            </DropdownMenuItem>\n          ) : null}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </>\n  )\n}\nfunction PDFViewerPageNumberControl({\n  activePage,\n  controlsDisabled,\n  numPages,\n  onPageChange,\n}: {\n  activePage: number\n  controlsDisabled: boolean\n  numPages: number\n  onPageChange: (pageNumber: number) => void\n}) {\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const displayPage = numPages ? activePage : 1\n  const [isEditing, setIsEditing] = React.useState(false)\n  const [draftPage, setDraftPage] = React.useState(() => String(displayPage))\n  React.useEffect(() => {\n    if (!isEditing) return\n    inputRef.current?.focus()\n    inputRef.current?.select()\n  }, [isEditing])\n  const applyPageDraft = React.useCallback(\n    (value: string) => {\n      const trimmedValue = value.trim()\n      if (!trimmedValue) return\n      const parsedPage = Number(trimmedValue)\n      if (!Number.isInteger(parsedPage)) return\n      onPageChange(Math.min(Math.max(parsedPage, 1), Math.max(numPages, 1)))\n    },\n    [numPages, onPageChange]\n  )\n  return (\n    <div className=\"flex items-center text-sm whitespace-nowrap text-primary\">\n      <span>Page</span>\n      {isEditing ? (\n        <Input\n          ref={inputRef}\n          aria-label=\"Page number\"\n          inputMode=\"numeric\"\n          pattern=\"[0-9]*\"\n          value={draftPage}\n          onBlur={() => setIsEditing(false)}\n          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {\n            const nextValue = event.target.value\n            setDraftPage(nextValue)\n            applyPageDraft(nextValue)\n          }}\n          onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {\n            if (event.key === \"Enter\" || event.key === \"Escape\") {\n              event.currentTarget.blur()\n            }\n          }}\n          className={cn(\n            \"h-8 px-2.5\",\n            \"mx-1 w-14 min-w-14 rounded-md [&_[data-slot=input]]:text-center\"\n          )}\n        />\n      ) : (\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"font-normal\"\n          aria-label={`Current page ${displayPage}. Edit page number`}\n          disabled={controlsDisabled || !numPages}\n          onClick={() => {\n            setDraftPage(String(displayPage))\n            setIsEditing(true)\n          }}\n        >\n          {displayPage}\n        </Button>\n      )}\n      <span>of {numPages || \"–\"}</span>\n    </div>\n  )\n}\nfunction PDFViewerSearchControl({\n  documentId,\n  controlsDisabled,\n}: {\n  documentId: string\n  controlsDisabled: boolean\n}) {\n  const { state, provides } = useSearch(documentId)\n  const { provides: scroll } = useScroll(documentId)\n  const [searchDraft, setSearchDraft] = React.useState(\"\")\n  const [searchQuery, setSearchQuery] = React.useState(\"\")\n  const [isSearching, setIsSearching] = React.useState(false)\n  const providesRef = React.useRef(provides)\n  const scrollRef = React.useRef(scroll)\n  const searchRequestIdRef = React.useRef(0)\n  const hasActiveQuery = Boolean(searchQuery.trim())\n  const resultLabel = isSearching\n    ? \"Searching\"\n    : !hasActiveQuery\n      ? \"No search\"\n      : state.total\n        ? `${state.activeResultIndex + 1} / ${state.total}`\n        : \"No results\"\n  const scrollToResult = React.useCallback(\n    (index: number) => {\n      const result = state.results[index]\n      if (!result || !scroll) return\n      const firstRect = result.rects[0]\n      scroll.scrollToPage({\n        pageNumber: result.pageIndex + 1,\n        ...(firstRect\n          ? {\n              pageCoordinates: {\n                x: firstRect.origin.x,\n                y: firstRect.origin.y,\n              },\n              alignY: 30,\n            }\n          : {}),\n        behavior: \"auto\",\n      })\n    },\n    [scroll, state.results]\n  )\n  React.useEffect(() => {\n    providesRef.current = provides\n    scrollRef.current = scroll\n  }, [provides, scroll])\n  const runSearch = React.useCallback((rawQuery: string) => {\n    const query = rawQuery.trim()\n    const requestId = searchRequestIdRef.current + 1\n    searchRequestIdRef.current = requestId\n    setSearchQuery(query)\n    const searchProvider = providesRef.current\n    const scrollProvider = scrollRef.current\n    if (!searchProvider) {\n      setIsSearching(false)\n      return\n    }\n    if (!query) {\n      searchProvider.stopSearch()\n      setIsSearching(false)\n      return\n    }\n    setIsSearching(true)\n    searchProvider.startSearch()\n    searchProvider.searchAllPages(query).wait(\n      (result) => {\n        if (searchRequestIdRef.current !== requestId) return\n        const firstResult = result.results[0]\n        if (firstResult && scrollProvider) {\n          searchProvider.goToResult(0)\n          const firstRect = firstResult.rects[0]\n          scrollProvider.scrollToPage({\n            pageNumber: firstResult.pageIndex + 1,\n            ...(firstRect\n              ? {\n                  pageCoordinates: {\n                    x: firstRect.origin.x,\n                    y: firstRect.origin.y,\n                  },\n                  alignY: 30,\n                }\n              : {}),\n            behavior: \"auto\",\n          })\n        }\n        setIsSearching(false)\n      },\n      () => {\n        if (searchRequestIdRef.current !== requestId) return\n        setIsSearching(false)\n      }\n    )\n  }, [])\n  React.useEffect(() => {\n    if (!searchDraft.trim()) return\n    const timeoutId = window.setTimeout(() => {\n      runSearch(searchDraft)\n    }, PDF_SEARCH_DEBOUNCE_MS)\n    return () => window.clearTimeout(timeoutId)\n  }, [runSearch, searchDraft])\n  const handleSearchDraftChange = React.useCallback(\n    (event: React.ChangeEvent<HTMLInputElement>) => {\n      const nextDraft = event.target.value\n      setSearchDraft(nextDraft)\n      if (nextDraft.trim()) {\n        setIsSearching(true)\n        return\n      }\n      searchRequestIdRef.current += 1\n      setSearchQuery(\"\")\n      setIsSearching(false)\n      provides?.stopSearch()\n    },\n    [provides]\n  )\n  const clearSearch = React.useCallback(() => {\n    searchRequestIdRef.current += 1\n    setSearchDraft(\"\")\n    setSearchQuery(\"\")\n    setIsSearching(false)\n    provides?.stopSearch()\n  }, [provides])\n  const navigate = React.useCallback(\n    (direction: 1 | -1) => {\n      if (!provides || state.total === 0) return\n      const index =\n        direction === 1 ? provides.nextResult() : provides.previousResult()\n      scrollToResult(index)\n    },\n    [provides, scrollToResult, state.total]\n  )\n  return (\n    <Popover>\n      <ToolbarTooltip label=\"Search text\">\n        <PopoverTrigger\n          render={\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label=\"Search text\"\n              disabled={controlsDisabled}\n            >\n              <IconPlaceholder\n                lucide=\"Search\"\n                tabler=\"IconSearch\"\n                hugeicons=\"Search01Icon\"\n                phosphor=\"MagnifyingGlassIcon\"\n                remixicon=\"RiSearchLine\"\n                className=\"size-4\"\n              />\n            </Button>\n          }\n        ></PopoverTrigger>\n      </ToolbarTooltip>\n      <PopoverContent align=\"end\" className=\"w-72\">\n        <div className=\"space-y-3\">\n          <Input\n            placeholder=\"Search text\"\n            value={searchDraft}\n            onChange={handleSearchDraftChange}\n            onKeyDown={(event) => {\n              if (event.key !== \"Enter\") return\n              event.preventDefault()\n              if (event.shiftKey && state.total) {\n                navigate(-1)\n              } else if (state.total) {\n                navigate(1)\n              } else if (searchDraft.trim()) {\n                runSearch(searchDraft)\n              }\n            }}\n          />\n          <div className=\"flex items-center justify-between gap-2\">\n            <div className=\"min-w-0 text-xs text-muted-foreground\">\n              <div className=\"truncate\">\n                {state.total ? (\n                  <>\n                    <span className=\"text-primary\">\n                      {state.activeResultIndex + 1}\n                    </span>\n                    {` / ${state.total}`}\n                  </>\n                ) : (\n                  resultLabel\n                )}\n              </div>\n            </div>\n            <div className=\"flex shrink-0 items-center gap-1\">\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"icon-sm\"\n                aria-label=\"Previous result\"\n                disabled={isSearching || state.total === 0}\n                onClick={() => navigate(-1)}\n              >\n                <IconPlaceholder\n                  lucide=\"ChevronLeft\"\n                  tabler=\"IconChevronLeft\"\n                  hugeicons=\"ArrowLeft01Icon\"\n                  phosphor=\"CaretLeftIcon\"\n                  remixicon=\"RiArrowLeftSLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                size=\"icon-sm\"\n                aria-label=\"Next result\"\n                disabled={isSearching || state.total === 0}\n                onClick={() => navigate(1)}\n              >\n                <IconPlaceholder\n                  lucide=\"ArrowRight\"\n                  tabler=\"IconArrowRight\"\n                  hugeicons=\"ArrowRight01Icon\"\n                  phosphor=\"ArrowRightIcon\"\n                  remixicon=\"RiArrowRightLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </div>\n          </div>\n          <div className=\"flex justify-end\">\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={clearSearch}\n            >\n              Clear\n            </Button>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\nfunction PDFViewerToolbar({\n  activePage,\n  controlsDisabled,\n  currentZoomLevel,\n  downloadDisabled = controlsDisabled,\n  isPreparingDownload = false,\n  numPages,\n  searchControl,\n  sidebarOpen,\n  showDownload,\n  showRotateControls,\n  showUpload,\n  toolbarActions,\n  onDownload,\n  onPageChange,\n  onRotate,\n  onToggleSidebar,\n  onUploadFile,\n  onZoomChange,\n}: {\n  activePage: number\n  controlsDisabled: boolean\n  currentZoomLevel: number\n  downloadDisabled?: boolean\n  isPreparingDownload?: boolean\n  numPages: number\n  searchControl: React.ReactNode\n  sidebarOpen: boolean\n  showDownload: boolean\n  showRotateControls: boolean\n  showUpload: boolean\n  toolbarActions?: React.ReactNode\n  onDownload?: () => void\n  onPageChange: (pageNumber: number) => void\n  onRotate: (direction: 1 | -1) => void\n  onToggleSidebar: () => void\n  onUploadFile?: (file: File) => void\n  onZoomChange: (zoomLevel: number) => void\n}) {\n  return (\n    <div className=\"flex min-h-12 flex-wrap items-center justify-between gap-2 border-b bg-background px-3 py-2\">\n      <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n        <TooltipProvider>\n          <ToolbarTooltip label=\"Pages sidebar\">\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              aria-label=\"Pages sidebar\"\n              aria-pressed={sidebarOpen}\n              data-active={sidebarOpen ? \"\" : undefined}\n              className={cn(\n                sidebarOpen &&\n                  \"bg-accent text-accent-foreground ring-1 ring-ring/40 ring-inset\"\n              )}\n              disabled={controlsDisabled}\n              onClick={onToggleSidebar}\n            >\n              <IconPlaceholder\n                lucide=\"PanelLeft\"\n                tabler=\"IconLayoutSidebar\"\n                hugeicons=\"SidebarLeftIcon\"\n                phosphor=\"SidebarIcon\"\n                remixicon=\"RiLayoutLeftLine\"\n                className=\"size-4\"\n              />\n            </Button>\n          </ToolbarTooltip>\n        </TooltipProvider>\n        <PDFViewerPageNumberControl\n          activePage={activePage}\n          controlsDisabled={controlsDisabled}\n          numPages={numPages}\n          onPageChange={onPageChange}\n        />\n      </div>\n      <TooltipProvider>\n        <div className=\"flex min-w-0 flex-wrap items-center justify-end gap-1\">\n          {showRotateControls ? (\n            <>\n              <div className=\"flex flex-none items-center gap-1\">\n                <ToolbarTooltip label=\"Rotate counterclockwise\">\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    aria-label=\"Rotate counterclockwise\"\n                    disabled={controlsDisabled}\n                    onClick={() => onRotate(-1)}\n                  >\n                    <IconPlaceholder\n                      lucide=\"RotateCw\"\n                      tabler=\"IconRotateClockwise\"\n                      hugeicons=\"RotateClockwiseIcon\"\n                      phosphor=\"ArrowClockwiseIcon\"\n                      remixicon=\"RiClockwiseLine\"\n                      className=\"size-4\"\n                    />\n                  </Button>\n                </ToolbarTooltip>\n                <ToolbarTooltip label=\"Rotate clockwise\">\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    aria-label=\"Rotate clockwise\"\n                    disabled={controlsDisabled}\n                    onClick={() => onRotate(1)}\n                  >\n                    <IconPlaceholder\n                      lucide=\"RotateCw\"\n                      tabler=\"IconRotateClockwise\"\n                      hugeicons=\"RotateClockwiseIcon\"\n                      phosphor=\"ArrowClockwiseIcon\"\n                      remixicon=\"RiClockwiseLine\"\n                      className=\"size-4 -scale-x-100\"\n                    />\n                  </Button>\n                </ToolbarTooltip>\n              </div>\n              <Separator\n                orientation=\"vertical\"\n                className=\"mx-1 h-4 self-center\"\n              />\n            </>\n          ) : null}\n          <div className=\"flex flex-none items-center gap-1\">\n            <ToolbarTooltip label=\"Zoom out\">\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label=\"Zoom out\"\n                disabled={\n                  controlsDisabled || currentZoomLevel <= ZOOM_OPTIONS[0]\n                }\n                onClick={() => {\n                  const nextZoom = [...ZOOM_OPTIONS]\n                    .reverse()\n                    .find((option) => option < currentZoomLevel)\n                  onZoomChange(nextZoom ?? ZOOM_OPTIONS[0])\n                }}\n              >\n                <IconPlaceholder\n                  lucide=\"CircleMinus\"\n                  tabler=\"IconCircleMinus\"\n                  hugeicons=\"MinusSignCircleIcon\"\n                  phosphor=\"MinusCircleIcon\"\n                  remixicon=\"RiIndeterminateCircleLine\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </ToolbarTooltip>\n            <Select\n              value={String(currentZoomLevel)}\n              onValueChange={(value) => onZoomChange(Number(value))}\n              disabled={controlsDisabled}\n              modal={false}\n            >\n              <SelectTrigger size=\"sm\" className=\"w-[84px] min-w-[84px]\">\n                <SelectValue placeholder=\"Zoom\">\n                  {Math.round(currentZoomLevel * 100)}%\n                </SelectValue>\n              </SelectTrigger>\n              <SelectContent alignItemWithTrigger={false}>\n                {ZOOM_OPTIONS.map((option) => (\n                  <SelectItem key={option} value={String(option)}>\n                    {Math.round(option * 100)}%\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n            <ToolbarTooltip label=\"Zoom in\">\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label=\"Zoom in\"\n                disabled={\n                  controlsDisabled ||\n                  currentZoomLevel >= ZOOM_OPTIONS[ZOOM_OPTIONS.length - 1]\n                }\n                onClick={() => {\n                  const nextZoom = ZOOM_OPTIONS.find(\n                    (option) => option > currentZoomLevel\n                  )\n                  onZoomChange(\n                    nextZoom ?? ZOOM_OPTIONS[ZOOM_OPTIONS.length - 1]\n                  )\n                }}\n              >\n                <IconPlaceholder\n                  lucide=\"CirclePlusIcon\"\n                  tabler=\"IconCirclePlusFilled\"\n                  hugeicons=\"PlusSignCircleIcon\"\n                  phosphor=\"PlusCircleIcon\"\n                  remixicon=\"RiAddCircleFill\"\n                  className=\"size-4\"\n                />\n              </Button>\n            </ToolbarTooltip>\n          </div>\n          <Separator orientation=\"vertical\" className=\"mx-1 h-4 self-center\" />\n          {searchControl}\n          {toolbarActions ? (\n            <>\n              <Separator\n                orientation=\"vertical\"\n                className=\"mx-1 h-4 self-center\"\n              />\n              {toolbarActions}\n            </>\n          ) : null}\n          {showDownload || showUpload ? (\n            <>\n              <Separator\n                orientation=\"vertical\"\n                className=\"mx-1 h-4 self-center\"\n              />\n              <PDFViewerFileActionsMenu\n                downloadDisabled={downloadDisabled}\n                isPreparingDownload={isPreparingDownload}\n                onDownload={onDownload}\n                onUploadFile={onUploadFile}\n                showDownload={showDownload}\n                showUpload={showUpload}\n              />\n            </>\n          ) : null}\n        </div>\n      </TooltipProvider>\n    </div>\n  )\n}\nfunction setPdfViewerRef<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (!ref) return\n  if (typeof ref === \"function\") {\n    ref(value)\n  } else {\n    ref.current = value\n  }\n}\nfunction PDFViewerScrollArea({\n  children,\n  className,\n  viewportClassName,\n  viewportProps,\n  viewportRef,\n}: {\n  children: React.ReactNode\n  className?: string\n  viewportClassName?: string\n  viewportProps?: React.HTMLAttributes<HTMLDivElement>\n  viewportRef?: React.Ref<HTMLDivElement>\n}) {\n  const resolveScrollAreaViewport = React.useContext(\n    PDFViewerScrollAreaResolverContext\n  )\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  const { className: viewportPropsClassName, ...resolvedViewportProps } =\n    viewportProps ?? {}\n  const setViewportRef = React.useCallback(\n    (viewport: HTMLDivElement | null) => {\n      setPdfViewerRef(viewportRef, viewport)\n    },\n    [viewportRef]\n  )\n  React.useLayoutEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n    const viewport = resolveScrollAreaViewport(container)\n    if (!viewport) {\n      console.error(\n        `PDFViewer could not resolve the scroll viewport. Add ${DEFAULT_SCROLL_AREA_VIEWPORT_SELECTOR} to your ScrollArea viewport or pass resolveScrollAreaViewport.`\n      )\n      return\n    }\n    setViewportRef(viewport)\n    return () => setViewportRef(null)\n  }, [resolveScrollAreaViewport, setViewportRef])\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"pdf-viewer-scroll-area\"\n      className={cn(\"size-full min-h-0\", className)}\n    >\n      <ScrollArea className=\"size-full min-h-0\">\n        <div\n          {...resolvedViewportProps}\n          data-slot=\"pdf-viewer-scroll-content\"\n          className={cn(\n            \"min-h-full\",\n            viewportPropsClassName,\n            viewportClassName\n          )}\n        >\n          {children}\n        </div>\n      </ScrollArea>\n    </div>\n  )\n}\nfunction PDFViewerThumbnails({\n  basePageRotations,\n  documentId,\n  activePage,\n  pageCount,\n  pageRotationDeltas,\n  pdfDocument,\n  selectedPageIndexes,\n  onSelectPage,\n}: {\n  basePageRotations: Rotation[]\n  documentId: string\n  activePage: number\n  pageCount: number\n  pageRotationDeltas: PageRotationDeltas\n  pdfDocument: PdfDocumentObject | null\n  selectedPageIndexes: Set<number>\n  onSelectPage: (pageNumber: number, mode: ThumbnailSelectionMode) => void\n}) {\n  const thumbnailListboxId = React.useId()\n  const activeDescendantId =\n    activePage > 0 ? `${thumbnailListboxId}-page-${activePage}` : undefined\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (pageCount < 1) return\n      const currentPage = activePage > 0 ? activePage : 1\n      let nextPage: number | null = null\n      if (event.key === \"ArrowDown\") {\n        nextPage = Math.min(pageCount, currentPage + 1)\n      } else if (event.key === \"ArrowUp\") {\n        nextPage = Math.max(1, currentPage - 1)\n      } else if (event.key === \"Home\") {\n        nextPage = 1\n      } else if (event.key === \"End\") {\n        nextPage = pageCount\n      } else if (event.key === \" \") {\n        event.preventDefault()\n        onSelectPage(currentPage, \"toggle\")\n        return\n      }\n      if (nextPage === null) return\n      event.preventDefault()\n      onSelectPage(nextPage, event.shiftKey ? \"range\" : \"replace\")\n    },\n    [activePage, onSelectPage, pageCount]\n  )\n  return (\n    <PDFViewerThumbnailScrollArea\n      activeDescendantId={activeDescendantId}\n      basePageRotations={basePageRotations}\n      documentId={documentId}\n      onKeyDown={handleKeyDown}\n      pageRotationDeltas={pageRotationDeltas}\n      pdfDocument={pdfDocument}\n    >\n      {(meta: ThumbMeta) => {\n        const pageNumber = meta.pageIndex + 1\n        const isActive = pageNumber === activePage\n        const isSelected = selectedPageIndexes.has(meta.pageIndex)\n        const imagePadding = meta.padding ?? 0\n        const pageRotationDelta = pageRotationDeltas.get(meta.pageIndex) ?? 0\n        const thumbnailImageStyle: React.CSSProperties =\n          pageRotationDelta % 2 === 1\n            ? {\n                height: meta.width,\n                transform: `rotate(${rotationToDegrees(pageRotationDelta)}deg)`,\n                width: meta.height,\n              }\n            : {\n                height: meta.height,\n                transform:\n                  pageRotationDelta === 0\n                    ? undefined\n                    : `rotate(${rotationToDegrees(pageRotationDelta)}deg)`,\n                width: meta.width,\n              }\n        return (\n          <div\n            key={meta.pageIndex}\n            data-pdf-viewer-thumbnail={pageNumber}\n            className={cn(\n              \"absolute right-0 left-0 flex justify-center\",\n              isActive && \"z-10\"\n            )}\n            style={{ top: meta.top, height: meta.wrapperHeight }}\n          >\n            <div\n              id={`${thumbnailListboxId}-page-${pageNumber}`}\n              role=\"option\"\n              data-pdf-viewer-thumbnail-option={pageNumber}\n              aria-current={isActive ? \"page\" : undefined}\n              aria-label={`Page ${pageNumber}`}\n              aria-posinset={pageNumber}\n              aria-selected={isSelected}\n              aria-setsize={pageCount}\n              data-selected={isSelected ? \"\" : undefined}\n              className={cn(\n                \"flex h-full w-full cursor-default flex-col items-center justify-between rounded-md px-2 py-0 text-xs transition-shadow outline-none select-none hover:bg-sidebar-accent\",\n                isActive || isSelected\n                  ? \"bg-sidebar-accent text-foreground\"\n                  : \"text-muted-foreground\",\n                isActive && THUMBNAIL_FOCUS_RING_CLASS\n              )}\n              onClick={(event) => {\n                const mode = event.shiftKey\n                  ? \"range\"\n                  : event.metaKey || event.ctrlKey\n                    ? \"toggle\"\n                    : \"replace\"\n                onSelectPage(pageNumber, mode)\n              }}\n            >\n              <span\n                className=\"mt-0 flex items-center justify-center overflow-hidden rounded-md bg-transparent\"\n                style={{\n                  width: meta.width + imagePadding * 2,\n                  height: meta.height + imagePadding * 2,\n                  padding: imagePadding,\n                }}\n              >\n                <ThumbImg\n                  documentId={documentId}\n                  meta={meta}\n                  className=\"block rounded-sm object-contain\"\n                  style={thumbnailImageStyle}\n                />\n              </span>\n              <span\n                className=\"flex items-center justify-center tabular-nums\"\n                style={{ height: meta.labelHeight }}\n              >\n                <span className=\"flex min-w-5 items-center justify-center px-1.5 text-center leading-5\">\n                  {pageNumber}\n                </span>\n              </span>\n            </div>\n          </div>\n        )\n      }}\n    </PDFViewerThumbnailScrollArea>\n  )\n}\nfunction PDFViewerThumbnailScrollArea({\n  activeDescendantId,\n  basePageRotations,\n  children,\n  documentId,\n  onKeyDown,\n  pageRotationDeltas,\n  pdfDocument,\n}: {\n  activeDescendantId?: string\n  basePageRotations: Rotation[]\n  children: (meta: ThumbMeta) => React.ReactNode\n  documentId: string\n  onKeyDown: React.KeyboardEventHandler<HTMLDivElement>\n  pageRotationDeltas: PageRotationDeltas\n  pdfDocument: PdfDocumentObject | null\n}) {\n  const { plugin: thumbnailPlugin } = useThumbnailPlugin()\n  const viewportRef = React.useRef<HTMLDivElement | null>(null)\n  const [viewportMetrics, setViewportMetrics] = React.useState({\n    clientHeight: 0,\n    scrollTop: 0,\n  })\n  const thumbnailScope = React.useMemo(\n    () => thumbnailPlugin?.provides().forDocument(documentId) ?? null,\n    [documentId, thumbnailPlugin]\n  )\n  const windowState = React.useSyncExternalStore(\n    React.useCallback(\n      (onStoreChange) => {\n        if (!thumbnailScope) return () => undefined\n        return thumbnailScope.onWindow(() => onStoreChange())\n      },\n      [thumbnailScope]\n    ),\n    React.useCallback(\n      () => thumbnailScope?.getWindow() ?? null,\n      [thumbnailScope]\n    ),\n    () => null\n  )\n  const hasWindowState = Boolean(windowState)\n  const paddingY = thumbnailPlugin?.cfg.paddingY ?? 0\n  const thumbnailLayout = React.useMemo(\n    () =>\n      buildThumbnailLayout({\n        basePageRotations,\n        pageRotationDeltas,\n        pdfDocument,\n        width: thumbnailPlugin?.cfg.width ?? THUMBNAIL_WIDTH,\n        gap: thumbnailPlugin?.cfg.gap ?? THUMBNAIL_GAP,\n        imagePadding: thumbnailPlugin?.cfg.imagePadding ?? 0,\n        labelHeight: thumbnailPlugin?.cfg.labelHeight ?? THUMBNAIL_LABEL_HEIGHT,\n        paddingY,\n      }),\n    [\n      basePageRotations,\n      pageRotationDeltas,\n      pdfDocument,\n      paddingY,\n      thumbnailPlugin,\n    ]\n  )\n  const effectiveWindowState = React.useMemo(() => {\n    if (!thumbnailLayout) return windowState\n    const items = getVisibleThumbnailItems({\n      buffer: thumbnailPlugin?.cfg.buffer ?? 3,\n      clientHeight: viewportMetrics.clientHeight,\n      items: thumbnailLayout.items,\n      scrollTop: viewportMetrics.scrollTop,\n    })\n    return {\n      start: items[0]?.pageIndex ?? -1,\n      end: items.at(-1)?.pageIndex ?? -1,\n      items,\n      totalHeight: thumbnailLayout.totalHeight,\n    }\n  }, [thumbnailLayout, thumbnailPlugin, viewportMetrics, windowState])\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !thumbnailScope) return\n    const updateWindow = () => {\n      setViewportMetrics({\n        clientHeight: viewport.clientHeight,\n        scrollTop: viewport.scrollTop,\n      })\n      thumbnailScope.updateWindow(viewport.scrollTop, viewport.clientHeight)\n    }\n    viewport.addEventListener(\"scroll\", updateWindow)\n    const frame = window.requestAnimationFrame(updateWindow)\n    return () => {\n      window.cancelAnimationFrame(frame)\n      viewport.removeEventListener(\"scroll\", updateWindow)\n    }\n  }, [thumbnailScope])\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !thumbnailScope) return\n    const resizeObserver = new ResizeObserver(() => {\n      setViewportMetrics({\n        clientHeight: viewport.clientHeight,\n        scrollTop: viewport.scrollTop,\n      })\n      thumbnailScope.updateWindow(viewport.scrollTop, viewport.clientHeight)\n    })\n    resizeObserver.observe(viewport)\n    return () => resizeObserver.disconnect()\n  }, [thumbnailScope])\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !thumbnailScope) return\n    thumbnailScope.updateWindow(viewport.scrollTop, viewport.clientHeight)\n  }, [thumbnailLayout, thumbnailScope, windowState])\n  React.useEffect(() => {\n    const viewport = viewportRef.current\n    if (!viewport || !thumbnailScope || !hasWindowState) return\n    return thumbnailScope.onScrollTo(({ top, behavior }) => {\n      viewport.scrollTo({ top, behavior })\n    })\n  }, [hasWindowState, thumbnailScope])\n  return (\n    <PDFViewerScrollArea\n      className=\"h-full w-full\"\n      viewportClassName=\"group/pdf-thumbnail-sidebar px-4 focus-visible:ring-0 focus-visible:ring-offset-0\"\n      viewportProps={{\n        \"aria-activedescendant\": activeDescendantId,\n        \"aria-label\": \"PDF pages\",\n        \"aria-multiselectable\": true,\n        onKeyDown,\n        onMouseDown: (event) => {\n          event.currentTarget.focus({ preventScroll: true })\n        },\n        role: \"listbox\",\n        style: {\n          paddingBottom: paddingY,\n          paddingTop: paddingY,\n        },\n        tabIndex: 0,\n      }}\n      viewportRef={viewportRef}\n    >\n      <div\n        className=\"relative\"\n        style={{ height: effectiveWindowState?.totalHeight ?? 0 }}\n      >\n        {effectiveWindowState?.items.map((meta) => children(meta))}\n      </div>\n    </PDFViewerScrollArea>\n  )\n}\nfunction PDFViewerScrollAreaViewport({\n  children,\n  className,\n  documentId,\n}: {\n  children: React.ReactNode\n  className?: string\n  documentId: string\n}) {\n  const viewportRef = useViewportRef(documentId)\n  const { provides: viewport } = useViewportCapability()\n  const isGated = useIsViewportGated(documentId)\n  const viewportGap = viewport?.getViewportGap() ?? 0\n  return (\n    <ViewportElementContext.Provider value={viewportRef}>\n      <PDFViewerScrollArea\n        className={className}\n        viewportClassName=\"relative select-none selection:bg-transparent selection:text-inherit\"\n        viewportProps={{\n          style: {\n            padding: viewportGap,\n          },\n        }}\n        viewportRef={viewportRef}\n      >\n        {isGated ? null : children}\n      </PDFViewerScrollArea>\n    </ViewportElementContext.Provider>\n  )\n}\n// Captures the scrollable viewport element so the imperative handle can expose\n// it.\nfunction PDFViewerViewportBridge({\n  viewportElementRef,\n}: {\n  viewportElementRef: React.MutableRefObject<HTMLDivElement | null>\n}) {\n  const elementRef = useViewportElement()\n  React.useEffect(() => {\n    viewportElementRef.current = elementRef?.current ?? null\n  })\n  return null\n}\nfunction PDFViewerTextSelectionLayer({\n  documentId,\n  pageIndex,\n  scale,\n}: {\n  documentId: string\n  pageIndex: number\n  scale: number\n}) {\n  const { plugin: selectionPlugin } = useSelectionPlugin()\n  const [rects, setRects] = React.useState<Rect[]>([])\n  React.useEffect(() => {\n    if (!selectionPlugin) return\n    return selectionPlugin.registerSelectionOnPage({\n      documentId,\n      pageIndex,\n      onRectsChange: ({ rects: nextRects }) => {\n        setRects(nextRects)\n      },\n    })\n  }, [documentId, pageIndex, selectionPlugin])\n  if (!rects.length) return null\n  return (\n    <>\n      {rects.map((rect, index) => (\n        <div\n          key={`${index}-${rect.origin.x}-${rect.origin.y}`}\n          className=\"pointer-events-none absolute\"\n          style={{\n            background: TEXT_SELECTION_BACKGROUND,\n            height: rect.size.height * scale,\n            left: rect.origin.x * scale,\n            top: rect.origin.y * scale,\n            width: rect.size.width * scale,\n          }}\n        />\n      ))}\n    </>\n  )\n}\nfunction PDFViewerSelectionReleaseGuard({\n  documentId,\n}: {\n  documentId: string\n}) {\n  const { plugin: selectionPlugin } = useSelectionPlugin()\n  const { provides: selection } = useSelectionCapability()\n  const lastSelectionModeIdRef = React.useRef<string | null>(null)\n  React.useEffect(() => {\n    if (!selection) return\n    return selection.forDocument(documentId).onBeginSelection(({ modeId }) => {\n      lastSelectionModeIdRef.current = modeId\n    })\n  }, [documentId, selection])\n  React.useEffect(() => {\n    if (!selection) return\n    let cleanupFrame = 0\n    const finalizeIfStillSelecting = () => {\n      window.cancelAnimationFrame(cleanupFrame)\n      cleanupFrame = window.requestAnimationFrame(() => {\n        const selectionState = selection.getState(documentId)\n        if (!selectionState.selecting) return\n        if (selectionState.selection && selectionPlugin) {\n          const pluginWithEndSelection = selectionPlugin as unknown as {\n            endSelection?: (documentId: string, modeId: string) => void\n          }\n          pluginWithEndSelection.endSelection?.(\n            documentId,\n            lastSelectionModeIdRef.current ?? \"pointerMode\"\n          )\n          return\n        }\n        if (!selectionState.selection) {\n          selection.clear(documentId)\n        }\n      })\n    }\n    window.addEventListener(\"pointerup\", finalizeIfStillSelecting)\n    window.addEventListener(\"pointercancel\", finalizeIfStillSelecting)\n    window.addEventListener(\"blur\", finalizeIfStillSelecting)\n    return () => {\n      window.cancelAnimationFrame(cleanupFrame)\n      window.removeEventListener(\"pointerup\", finalizeIfStillSelecting)\n      window.removeEventListener(\"pointercancel\", finalizeIfStillSelecting)\n      window.removeEventListener(\"blur\", finalizeIfStillSelecting)\n    }\n  }, [documentId, selection, selectionPlugin])\n  return null\n}\nfunction isEditableCopyTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false\n  if (target.isContentEditable) return true\n  return Boolean(target.closest(\"input, textarea, [contenteditable='true']\"))\n}\nfunction PDFViewerSelectionCopyShortcut({\n  documentId,\n}: {\n  documentId: string\n}) {\n  const { provides: selection } = useSelectionCapability()\n  React.useEffect(() => {\n    if (!selection) return\n    const copySelectedPdfText = (event: Event) => {\n      if (isEditableCopyTarget(event.target)) return\n      if (!selection.getState(documentId).selection) return\n      event.preventDefault()\n      selection.copyToClipboard(documentId)\n    }\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key.toLowerCase() !== \"c\") return\n      if (!event.metaKey && !event.ctrlKey) return\n      copySelectedPdfText(event)\n    }\n    document.addEventListener(\"copy\", copySelectedPdfText)\n    document.addEventListener(\"keydown\", handleKeyDown)\n    return () => {\n      document.removeEventListener(\"copy\", copySelectedPdfText)\n      document.removeEventListener(\"keydown\", handleKeyDown)\n    }\n  }, [documentId, selection])\n  return null\n}\nfunction isQuarterTurn(rotation: Rotation) {\n  return rotation % 2 === 1\n}\nfunction getRotatedDimensions({\n  height,\n  rotation,\n  width,\n}: {\n  height: number\n  rotation: Rotation\n  width: number\n}) {\n  return isQuarterTurn(rotation)\n    ? { height: width, width: height }\n    : { height, width }\n}\nfunction getRotatedPageDimensions(page: PageLayout, rotation: Rotation) {\n  return getRotatedDimensions({\n    height: page.height,\n    rotation,\n    width: page.width,\n  })\n}\nfunction applyPageRotationDeltasToScrollerLayout({\n  basePageRotations,\n  layout,\n  pageRotationDeltas,\n}: {\n  basePageRotations: Rotation[]\n  layout: ScrollerLayout\n  pageRotationDeltas: PageRotationDeltas\n}): ScrollerLayout {\n  if (pageRotationDeltas.size === 0) return layout\n  let maxWidth = 0\n  let maxHeight = 0\n  let offset = 0\n  const pageGap = layout.pageGap\n  let startSpacingAdjustment = 0\n  const items: VirtualItem[] = layout.items.map((item, itemIndex) => {\n    let pageOffset = 0\n    let itemWidth = 0\n    let itemHeight = 0\n    const pageLayouts = item.pageLayouts.map((page) => {\n      const basePageRotation =\n        basePageRotations[page.pageIndex] ?? normalizeRotation(0)\n      const pageRotation = normalizeRotation(\n        basePageRotation + (pageRotationDeltas.get(page.pageIndex) ?? 0)\n      )\n      const rotatedSize = getRotatedPageDimensions(page, pageRotation)\n      const oldScrollAxisSize =\n        layout.strategy === ScrollStrategy.Horizontal\n          ? page.rotatedWidth\n          : page.rotatedHeight\n      const newScrollAxisSize =\n        layout.strategy === ScrollStrategy.Horizontal\n          ? rotatedSize.width\n          : rotatedSize.height\n      if (\n        layout.startSpacing === 0 &&\n        itemIndex === 0 &&\n        pageOffset === 0 &&\n        newScrollAxisSize < oldScrollAxisSize\n      ) {\n        startSpacingAdjustment = Math.max(\n          startSpacingAdjustment,\n          (oldScrollAxisSize - newScrollAxisSize) / 2\n        )\n      }\n      const nextPageLayout = {\n        ...page,\n        rotatedHeight: rotatedSize.height,\n        rotatedWidth: rotatedSize.width,\n        x: layout.strategy === ScrollStrategy.Horizontal ? 0 : pageOffset,\n        y: layout.strategy === ScrollStrategy.Horizontal ? pageOffset : 0,\n      }\n      pageOffset +=\n        (layout.strategy === ScrollStrategy.Horizontal\n          ? rotatedSize.height\n          : rotatedSize.width) + pageGap\n      itemWidth =\n        layout.strategy === ScrollStrategy.Horizontal\n          ? Math.max(itemWidth, rotatedSize.width)\n          : itemWidth + rotatedSize.width\n      itemHeight =\n        layout.strategy === ScrollStrategy.Horizontal\n          ? itemHeight + rotatedSize.height\n          : Math.max(itemHeight, rotatedSize.height)\n      return nextPageLayout\n    })\n    if (pageLayouts.length > 1) {\n      if (layout.strategy === ScrollStrategy.Horizontal) {\n        itemHeight -= pageGap\n      } else {\n        itemWidth -= pageGap\n      }\n    }\n    const nextItem = {\n      ...item,\n      height: itemHeight,\n      offset,\n      pageLayouts,\n      width: itemWidth,\n      x: layout.strategy === ScrollStrategy.Horizontal ? offset : item.x,\n      y: layout.strategy === ScrollStrategy.Horizontal ? item.y : offset,\n    }\n    if (layout.strategy === ScrollStrategy.Horizontal) {\n      offset += itemWidth + pageGap\n      maxHeight = Math.max(maxHeight, itemHeight)\n    } else {\n      offset += itemHeight + pageGap\n      maxWidth = Math.max(maxWidth, itemWidth)\n    }\n    return nextItem\n  })\n  if (items.length > 0) {\n    offset -= pageGap\n  }\n  return {\n    ...layout,\n    endSpacing: layout.endSpacing,\n    items,\n    startSpacing: layout.startSpacing + startSpacingAdjustment,\n    totalHeight:\n      layout.strategy === ScrollStrategy.Horizontal\n        ? maxHeight\n        : layout.startSpacing +\n          startSpacingAdjustment +\n          offset +\n          layout.endSpacing,\n    totalWidth:\n      layout.strategy === ScrollStrategy.Horizontal\n        ? layout.startSpacing +\n          startSpacingAdjustment +\n          offset +\n          layout.endSpacing\n        : maxWidth,\n  }\n}\nfunction PDFViewerScroller({\n  documentId,\n  pageRotationDeltas,\n  basePageRotations,\n  renderPage,\n}: {\n  documentId: string\n  pageRotationDeltas: PageRotationDeltas\n  basePageRotations: Rotation[]\n  renderPage: (props: PageLayout) => React.ReactNode\n}) {\n  const { plugin: scrollPlugin } = useScrollPlugin()\n  const [layoutData, setLayoutData] = React.useState<{\n    docId: string | null\n    layout: ScrollerLayout | null\n  }>({ docId: null, layout: null })\n  React.useEffect(() => {\n    if (!scrollPlugin || !documentId) return\n    let frame = 0\n    const setCurrentLayout = () => {\n      try {\n        setLayoutData({\n          docId: documentId,\n          layout: scrollPlugin.getScrollerLayout(documentId),\n        })\n      } catch {\n        setLayoutData({ docId: documentId, layout: null })\n      }\n    }\n    const unsubscribe = scrollPlugin.onScrollerData(documentId, (layout) => {\n      setLayoutData({ docId: documentId, layout })\n    })\n    frame = window.requestAnimationFrame(setCurrentLayout)\n    return () => {\n      window.cancelAnimationFrame(frame)\n      unsubscribe()\n      setLayoutData({ docId: null, layout: null })\n      scrollPlugin.clearLayoutReady(documentId)\n    }\n  }, [documentId, scrollPlugin])\n  const scrollerLayout = React.useMemo(() => {\n    if (layoutData.docId !== documentId || !layoutData.layout) return null\n    return applyPageRotationDeltasToScrollerLayout({\n      basePageRotations,\n      layout: layoutData.layout,\n      pageRotationDeltas,\n    })\n  }, [basePageRotations, documentId, layoutData, pageRotationDeltas])\n  React.useLayoutEffect(() => {\n    if (!scrollPlugin || !documentId || !scrollerLayout) return\n    scrollPlugin.setLayoutReady(documentId)\n  }, [documentId, scrollPlugin, scrollerLayout])\n  if (!scrollerLayout) return null\n  return (\n    <div\n      style={{\n        width: `${scrollerLayout.totalWidth}px`,\n        height: `${scrollerLayout.totalHeight}px`,\n        position: \"relative\",\n        boxSizing: \"border-box\",\n        margin: \"0 auto\",\n        ...(scrollerLayout.strategy === ScrollStrategy.Horizontal && {\n          display: \"flex\",\n          flexDirection: \"row\",\n        }),\n      }}\n    >\n      <div\n        style={\n          scrollerLayout.strategy === ScrollStrategy.Horizontal\n            ? {\n                width: scrollerLayout.startSpacing,\n                height: \"100%\",\n                flexShrink: 0,\n              }\n            : {\n                height: scrollerLayout.startSpacing,\n                width: \"100%\",\n              }\n        }\n      />\n      <div\n        style={{\n          gap: scrollerLayout.pageGap,\n          display: \"flex\",\n          alignItems: \"center\",\n          position: \"relative\",\n          boxSizing: \"border-box\",\n          ...(scrollerLayout.strategy === ScrollStrategy.Horizontal\n            ? {\n                flexDirection: \"row\",\n                minHeight: \"100%\",\n              }\n            : {\n                flexDirection: \"column\",\n                minWidth: \"fit-content\",\n              }),\n        }}\n      >\n        {scrollerLayout.items.map((item) => (\n          <div\n            key={item.pageNumbers[0]}\n            style={{\n              display: \"flex\",\n              justifyContent: \"center\",\n              gap: scrollerLayout.pageGap,\n            }}\n          >\n            {item.pageLayouts.map((layout) => (\n              <div\n                key={layout.pageNumber}\n                style={{\n                  width: `${layout.rotatedWidth}px`,\n                  height: `${layout.rotatedHeight}px`,\n                  position: \"relative\",\n                  zIndex: layout.elevated ? 1 : undefined,\n                }}\n              >\n                {renderPage(layout)}\n              </div>\n            ))}\n          </div>\n        ))}\n      </div>\n      <div\n        style={\n          scrollerLayout.strategy === ScrollStrategy.Horizontal\n            ? {\n                width: scrollerLayout.endSpacing,\n                height: \"100%\",\n                flexShrink: 0,\n              }\n            : {\n                height: scrollerLayout.endSpacing,\n                width: \"100%\",\n              }\n        }\n      />\n    </div>\n  )\n}\ntype PDFViewerInnerProps = {\n  viewerRef: React.ForwardedRef<PDFViewerHandle>\n  pdfFile: string\n  documentId: string\n  document: PdfDocumentObject | null\n  defaultZoom: number\n  className?: string\n  fileName?: string\n  showDownload: boolean\n  showToolbar: boolean\n  showRotateControls: boolean\n  showUpload: boolean\n  toolbarActions?: React.ReactNode\n  pageClassName?: (pageNumber: number) => string | undefined\n  renderPageOverlay?: (props: PDFViewerPageOverlayProps) => React.ReactNode\n  onActivePageChange?: (pageNumber: number) => void\n  onPdfUpload?: (file: File) => void\n  onPagePointerDown?: PDFViewerProps[\"onPagePointerDown\"]\n  onPagePointerMove?: PDFViewerProps[\"onPagePointerMove\"]\n  onPagePointerUp?: PDFViewerProps[\"onPagePointerUp\"]\n  onPagePointerCancel?: PDFViewerProps[\"onPagePointerCancel\"]\n  onUploadFile: (file: File) => void\n}\nfunction PDFViewerInner({\n  viewerRef,\n  pdfFile,\n  documentId,\n  document: pdfDocument,\n  defaultZoom,\n  className,\n  fileName,\n  showDownload,\n  showToolbar,\n  showRotateControls,\n  showUpload,\n  toolbarActions,\n  pageClassName,\n  renderPageOverlay,\n  onActivePageChange,\n  onPdfUpload,\n  onPagePointerDown,\n  onPagePointerMove,\n  onPagePointerUp,\n  onPagePointerCancel,\n  onUploadFile,\n}: PDFViewerInnerProps) {\n  const { registry } = useRegistry()\n  const { state: scrollState, provides: scroll } = useScroll(documentId)\n  const { state: zoomState, provides: zoom } = useZoom(documentId)\n  const { provides: thumbnails } = useThumbnailCapability()\n  const { plugin: thumbnailPlugin } = useThumbnailPlugin()\n  const [sidebarOpen, setSidebarOpen] = React.useState(false)\n  const [isPreparingDownload, setIsPreparingDownload] = React.useState(false)\n  const [pageRotationDeltas, setPageRotationDeltas] =\n    React.useState<PageRotationDeltas>(() => new Map())\n  const [selectedPageIndexes, setSelectedPageIndexes] = React.useState<\n    Set<number>\n  >(() => new Set())\n  const basePageRotations = React.useMemo(\n    () =>\n      pdfDocument?.pages.map((page) => normalizeRotation(page.rotation)) ?? [],\n    [pdfDocument]\n  )\n  const [viewerShellRef, viewerShellWidth] = useElementWidth<HTMLDivElement>()\n  const sidebarInline = useInlineThumbnailSidebar(viewerShellWidth)\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null)\n  const pageRotationDeltasRef = React.useRef(pageRotationDeltas)\n  const selectedPageIndexesRef = React.useRef(selectedPageIndexes)\n  const selectionAnchorPageIndexRef = React.useRef<number | null>(null)\n  const suppressActivePageSelectionSyncRef = React.useRef<number | null>(null)\n  const initializedSelectionDocumentRef = React.useRef<string | null>(null)\n  const activePage = scrollState.currentPage\n  const numPages = pdfDocument?.pageCount ?? 0\n  const isLoading = !pdfDocument\n  const controlsDisabled = !numPages\n  const downloadDisabled = controlsDisabled || isPreparingDownload\n  const thumbnailSidebarVisible = sidebarOpen && !isLoading\n  const currentZoomLevel = zoomState.currentZoomLevel\n  const alignedThumbnailSidebarDocumentRef = React.useRef<string | null>(null)\n  React.useEffect(() => {\n    pageRotationDeltasRef.current = pageRotationDeltas\n  }, [pageRotationDeltas])\n  React.useEffect(() => {\n    selectedPageIndexesRef.current = selectedPageIndexes\n  }, [selectedPageIndexes])\n  React.useEffect(() => {\n    if (activePage > 0) onActivePageChange?.(activePage)\n  }, [activePage, onActivePageChange])\n  React.useEffect(() => {\n    if (activePage < 1 || numPages < 1) return\n    const activePageIndex = activePage - 1\n    const suppressedPageIndex = suppressActivePageSelectionSyncRef.current\n    suppressActivePageSelectionSyncRef.current = null\n    if (suppressedPageIndex === activePageIndex) return\n    const nextSelection = new Set([activePageIndex])\n    selectionAnchorPageIndexRef.current = activePageIndex\n    selectedPageIndexesRef.current = nextSelection\n    setSelectedPageIndexes((previousSelection) =>\n      arePageIndexSetsEqual(previousSelection, nextSelection)\n        ? previousSelection\n        : nextSelection\n    )\n  }, [activePage, numPages])\n  React.useEffect(() => {\n    if (\n      numPages < 1 ||\n      initializedSelectionDocumentRef.current === documentId\n    ) {\n      return\n    }\n    const initialPageIndex = Math.max(0, (activePage > 0 ? activePage : 1) - 1)\n    const initialSelection = new Set([initialPageIndex])\n    initializedSelectionDocumentRef.current = documentId\n    selectionAnchorPageIndexRef.current = initialPageIndex\n    selectedPageIndexesRef.current = initialSelection\n    setSelectedPageIndexes(initialSelection)\n  }, [activePage, documentId, numPages])\n  React.useEffect(() => {\n    if (!thumbnailSidebarVisible) {\n      alignedThumbnailSidebarDocumentRef.current = null\n      return\n    }\n    if (\n      activePage < 1 ||\n      !thumbnails ||\n      alignedThumbnailSidebarDocumentRef.current === documentId\n    ) {\n      return\n    }\n    alignedThumbnailSidebarDocumentRef.current = documentId\n    const frame = window.requestAnimationFrame(() => {\n      thumbnails.forDocument(documentId).scrollToThumb(activePage - 1)\n    })\n    return () => window.cancelAnimationFrame(frame)\n  }, [activePage, documentId, thumbnailSidebarVisible, thumbnails])\n  // The zoom plugin only releases its viewport gate for mode-based zoom\n  // levels (automatic/fit); with a numeric default the gate would never\n  // lift, so apply the initial zoom explicitly once the document loads.\n  const initialZoomDocumentRef = React.useRef<string | null>(null)\n  React.useEffect(() => {\n    if (!pdfDocument || !zoom) return\n    if (initialZoomDocumentRef.current === documentId) return\n    initialZoomDocumentRef.current = documentId\n    zoom.requestZoom(defaultZoom)\n  }, [defaultZoom, documentId, pdfDocument, zoom])\n  const scrollToPage = React.useCallback(\n    (pageNumber: number, options?: ScrollIntoViewOptions) => {\n      scroll?.scrollToPage({\n        pageNumber,\n        behavior: options?.behavior === \"smooth\" ? \"smooth\" : \"auto\",\n      })\n    },\n    [scroll]\n  )\n  const selectThumbnailPage = React.useCallback(\n    (pageNumber: number, mode: ThumbnailSelectionMode) => {\n      const pageIndex = pageNumber - 1\n      if (pageIndex < 0 || pageIndex >= numPages) return\n      suppressActivePageSelectionSyncRef.current = pageIndex\n      setSelectedPageIndexes((previousSelection) => {\n        let nextSelection: Set<number>\n        if (mode === \"range\") {\n          const anchorPageIndex =\n            selectionAnchorPageIndexRef.current ??\n            (activePage > 0 ? activePage - 1 : pageIndex)\n          nextSelection = getPageIndexRange(anchorPageIndex, pageIndex)\n        } else if (mode === \"toggle\") {\n          nextSelection = new Set(previousSelection)\n          if (nextSelection.has(pageIndex)) {\n            nextSelection.delete(pageIndex)\n          } else {\n            nextSelection.add(pageIndex)\n          }\n          selectionAnchorPageIndexRef.current = pageIndex\n        } else {\n          nextSelection = new Set([pageIndex])\n          selectionAnchorPageIndexRef.current = pageIndex\n        }\n        selectedPageIndexesRef.current = nextSelection\n        return nextSelection\n      })\n      scrollToPage(pageNumber)\n    },\n    [activePage, numPages, scrollToPage]\n  )\n  React.useImperativeHandle(\n    viewerRef,\n    () => ({\n      scrollToPage,\n      scrollToPageArea: (pageNumber, area, options) => {\n        const pageSize = pdfDocument?.pages[pageNumber - 1]?.size\n        scroll?.scrollToPage({\n          pageNumber,\n          ...(pageSize\n            ? {\n                pageCoordinates: {\n                  x: ((area.left ?? 0) / 100) * pageSize.width,\n                  y: (area.top / 100) * pageSize.height,\n                },\n                alignY: 25,\n              }\n            : {}),\n          behavior: options?.behavior === \"smooth\" ? \"smooth\" : \"auto\",\n        })\n      },\n      getViewportElement: () => viewportElementRef.current,\n    }),\n    [pdfDocument, scroll, scrollToPage]\n  )\n  const handleDownload = React.useCallback(async () => {\n    if (!pdfFile || isPreparingDownload) return\n    setIsPreparingDownload(true)\n    try {\n      await downloadPdfWithPageRotations({\n        fileName: getPdfDownloadFileName(fileName, pdfFile),\n        pageRotationDeltas,\n        src: pdfFile,\n      })\n    } catch (error) {\n      console.error(error)\n    } finally {\n      setIsPreparingDownload(false)\n    }\n  }, [fileName, isPreparingDownload, pageRotationDeltas, pdfFile])\n  const rotateSelectedPages = React.useCallback(\n    (direction: -1 | 1) => {\n      if (!pdfDocument || !registry || activePage < 1) return\n      const documentState = registry.getStore().getState().core.documents[\n        documentId\n      ]\n      const currentDocument = documentState?.document ?? pdfDocument\n      const selectedTargetPageIndexes = Array.from(\n        selectedPageIndexesRef.current\n      ).filter((pageIndex) => currentDocument.pages[pageIndex])\n      const fallbackPageIndex = activePage - 1\n      const targetPageIndexes = (\n        selectedTargetPageIndexes.length\n          ? selectedTargetPageIndexes\n          : [fallbackPageIndex]\n      )\n        .filter((pageIndex) => currentDocument.pages[pageIndex])\n        .sort((a, b) => a - b)\n      if (targetPageIndexes.length === 0) return\n      const previousDeltas = pageRotationDeltasRef.current\n      const nextDeltas = new Map(previousDeltas)\n      const referencePageIndex =\n        activePage > 0 && currentDocument.pages[activePage - 1]\n          ? activePage - 1\n          : targetPageIndexes[0]\n      let scrollDelta = 0\n      for (const pageIndex of targetPageIndexes) {\n        const currentPage = currentDocument.pages[pageIndex]\n        if (!currentPage) continue\n        const previousDelta = previousDeltas.get(pageIndex) ?? 0\n        const nextDelta = normalizeRotation(previousDelta + direction)\n        const basePageRotation =\n          basePageRotations[pageIndex] ??\n          normalizeRotation(currentPage.rotation)\n        const previousPageRotation = normalizeRotation(\n          basePageRotation + previousDelta\n        )\n        const nextPageRotation = normalizeRotation(basePageRotation + nextDelta)\n        const previousRotatedSize = getRotatedDimensions({\n          height: currentPage.size.height * currentZoomLevel,\n          rotation: previousPageRotation,\n          width: currentPage.size.width * currentZoomLevel,\n        })\n        const nextRotatedSize = getRotatedDimensions({\n          height: currentPage.size.height * currentZoomLevel,\n          rotation: nextPageRotation,\n          width: currentPage.size.width * currentZoomLevel,\n        })\n        const heightDelta = nextRotatedSize.height - previousRotatedSize.height\n        if (pageIndex < referencePageIndex) {\n          scrollDelta += heightDelta\n        } else if (pageIndex === referencePageIndex) {\n          scrollDelta += heightDelta / 2\n        }\n        if (nextDelta) {\n          nextDeltas.set(pageIndex, nextDelta)\n        } else {\n          nextDeltas.delete(pageIndex)\n        }\n      }\n      const store = registry.getStore()\n      const viewport = viewportElementRef.current\n      pageRotationDeltasRef.current = nextDeltas\n      flushSync(() => {\n        setPageRotationDeltas(nextDeltas)\n        store.dispatchToCore(refreshPages(documentId, targetPageIndexes))\n      })\n      if (viewport && scrollDelta !== 0) {\n        viewport.scrollTop += scrollDelta\n      }\n      ;(\n        thumbnailPlugin as {\n          calculateWindowState?: (documentId: string) => void\n        } | null\n      )?.calculateWindowState?.(documentId)\n    },\n    [\n      activePage,\n      basePageRotations,\n      currentZoomLevel,\n      documentId,\n      pdfDocument,\n      registry,\n      thumbnailPlugin,\n    ]\n  )\n  const handleUpload = React.useCallback(\n    (file: File) => {\n      onUploadFile(file)\n      onPdfUpload?.(file)\n    },\n    [onPdfUpload, onUploadFile]\n  )\n  const renderPage = React.useCallback(\n    (page: PageLayout) => {\n      const pageNumber = page.pageNumber\n      const basePageRotation =\n        basePageRotations[page.pageIndex] ??\n        pdfDocument?.pages[page.pageIndex]?.rotation ??\n        normalizeRotation(0)\n      const pageRotation = normalizeRotation(\n        basePageRotation + (pageRotationDeltas.get(page.pageIndex) ?? 0)\n      )\n      return (\n        <Rotate\n          documentId={documentId}\n          pageIndex={page.pageIndex}\n          rotation={pageRotation}\n        >\n          <PagePointerProvider\n            documentId={documentId}\n            pageIndex={page.pageIndex}\n            rotation={pageRotation}\n            key={`${page.pageIndex}-${pageRotation}`}\n            data-pdf-viewer-page={pageNumber}\n            className={cn(\n              \"relative border border-transparent bg-transparent shadow-xs select-none selection:bg-transparent selection:text-inherit\",\n              pageClassName?.(pageNumber)\n            )}\n            style={{ backgroundColor: \"transparent\" }}\n            onPointerDown={(event: React.PointerEvent<HTMLDivElement>) =>\n              onPagePointerDown?.(event, pageNumber)\n            }\n            onPointerMove={(event: React.PointerEvent<HTMLDivElement>) =>\n              onPagePointerMove?.(event, pageNumber)\n            }\n            onPointerUp={(event: React.PointerEvent<HTMLDivElement>) =>\n              onPagePointerUp?.(event, pageNumber)\n            }\n            onPointerCancel={(event: React.PointerEvent<HTMLDivElement>) =>\n              onPagePointerCancel?.(event, pageNumber)\n            }\n          >\n            <div\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 bg-white\"\n            />\n            <RenderLayer\n              documentId={documentId}\n              pageIndex={page.pageIndex}\n              scale={Math.min(currentZoomLevel, PAGE_BASE_RENDER_MAX_SCALE)}\n              dpr={PAGE_BASE_RENDER_DPR}\n              className=\"pointer-events-none absolute inset-0 h-full w-full object-fill opacity-100 blur-[0.35px] transition-none\"\n            />\n            <TilingLayer\n              documentId={documentId}\n              pageIndex={page.pageIndex}\n              key={`tiles-${page.pageIndex}-${pageRotation}`}\n              className=\"pointer-events-none opacity-100 transition-none [&_img]:opacity-100 [&_img]:transition-none\"\n            />\n            <SearchLayer\n              documentId={documentId}\n              pageIndex={page.pageIndex}\n              className=\"pointer-events-none\"\n              highlightColor=\"rgba(253, 224, 71, 0.45)\"\n              activeHighlightColor=\"rgba(249, 115, 22, 0.55)\"\n            />\n            <PDFViewerTextSelectionLayer\n              documentId={documentId}\n              pageIndex={page.pageIndex}\n              scale={currentZoomLevel}\n            />\n            {renderPageOverlay?.({\n              pageNumber,\n              pageWidth: page.width,\n              pageHeight: page.height,\n              scale: currentZoomLevel,\n              rotation: rotationToDegrees(pageRotation),\n            })}\n          </PagePointerProvider>\n        </Rotate>\n      )\n    },\n    [\n      basePageRotations,\n      currentZoomLevel,\n      onPagePointerCancel,\n      onPagePointerDown,\n      onPagePointerMove,\n      onPagePointerUp,\n      pageClassName,\n      pageRotationDeltas,\n      renderPageOverlay,\n      documentId,\n      pdfDocument,\n    ]\n  )\n  return (\n    <div\n      data-slot=\"pdf-viewer\"\n      className={cn(\n        \"flex h-full max-h-full min-h-0 w-full flex-col overflow-hidden bg-background\",\n        className\n      )}\n    >\n      {showToolbar ? (\n        <PDFViewerToolbar\n          activePage={activePage}\n          controlsDisabled={controlsDisabled}\n          currentZoomLevel={currentZoomLevel}\n          downloadDisabled={downloadDisabled}\n          isPreparingDownload={isPreparingDownload}\n          numPages={numPages}\n          searchControl={\n            <PDFViewerSearchControl\n              key={documentId}\n              documentId={documentId}\n              controlsDisabled={controlsDisabled}\n            />\n          }\n          sidebarOpen={sidebarOpen}\n          showDownload={showDownload}\n          showRotateControls={showRotateControls}\n          showUpload={showUpload}\n          toolbarActions={toolbarActions}\n          onDownload={handleDownload}\n          onPageChange={scrollToPage}\n          onRotate={rotateSelectedPages}\n          onToggleSidebar={() => setSidebarOpen((open) => !open)}\n          onUploadFile={handleUpload}\n          onZoomChange={(zoomLevel) => zoom?.requestZoom(zoomLevel)}\n        />\n      ) : null}\n      <div\n        ref={viewerShellRef}\n        className=\"relative flex min-h-0 flex-1 overflow-hidden bg-muted/30\"\n      >\n        {isLoading ? (\n          <PDFViewerLoadingSkeleton\n            sidebarInline={sidebarInline}\n            sidebarOpen={sidebarOpen}\n          />\n        ) : null}\n        <div className=\"flex h-full max-h-full min-h-0 w-full flex-1 overflow-hidden\">\n          <DocumentViewerThumbnailSidebar\n            closedInlineClassName={THUMBNAIL_SIDEBAR_CLOSED_CLASS}\n            inline={sidebarInline}\n            open={thumbnailSidebarVisible}\n            widthClassName={THUMBNAIL_SIDEBAR_WIDTH_CLASS}\n          >\n            {thumbnailSidebarVisible ? (\n              <PDFViewerThumbnails\n                basePageRotations={basePageRotations}\n                documentId={documentId}\n                activePage={activePage}\n                pageCount={numPages}\n                pageRotationDeltas={pageRotationDeltas}\n                pdfDocument={pdfDocument}\n                selectedPageIndexes={selectedPageIndexes}\n                onSelectPage={selectThumbnailPage}\n              />\n            ) : null}\n          </DocumentViewerThumbnailSidebar>\n          <PDFViewerScrollAreaViewport\n            documentId={documentId}\n            className=\"relative h-full max-h-full min-h-0 min-w-0 flex-1\"\n          >\n            <PDFViewerViewportBridge viewportElementRef={viewportElementRef} />\n            <PDFViewerSelectionCopyShortcut documentId={documentId} />\n            <PDFViewerSelectionReleaseGuard documentId={documentId} />\n            <GlobalPointerProvider documentId={documentId}>\n              <PDFViewerScroller\n                basePageRotations={basePageRotations}\n                documentId={documentId}\n                pageRotationDeltas={pageRotationDeltas}\n                renderPage={renderPage}\n              />\n            </GlobalPointerProvider>\n            <CopyToClipboard />\n          </PDFViewerScrollAreaViewport>\n        </div>\n      </div>\n    </div>\n  )\n}\nfunction PDFViewerDocumentLoader({\n  pdfFile,\n  onDocumentLoadSuccess,\n  ...innerProps\n}: {\n  pdfFile: string\n  onDocumentLoadSuccess?: (numPages: number) => void\n} & Omit<PDFViewerInnerProps, \"pdfFile\" | \"documentId\" | \"document\">) {\n  const { provides: documentManager } = useDocumentManagerCapability()\n  const { activeDocumentId, activeDocument } = useActiveDocument()\n  const [loadError, setLoadError] = React.useState(false)\n  const openedFileRef = React.useRef<string | null>(null)\n  const onDocumentLoadSuccessRef = React.useRef(onDocumentLoadSuccess)\n  React.useEffect(() => {\n    onDocumentLoadSuccessRef.current = onDocumentLoadSuccess\n  })\n  React.useEffect(() => {\n    if (!documentManager || !pdfFile) return\n    if (openedFileRef.current === pdfFile) return\n    openedFileRef.current = pdfFile\n    setLoadError(false)\n    const previousDocumentIds = documentManager\n      .getOpenDocuments()\n      .map((openDocument) => openDocument.id)\n    const handleOpenError = () => {\n      if (openedFileRef.current === pdfFile) setLoadError(true)\n    }\n    documentManager\n      .openDocumentUrl({\n        url: pdfFile,\n        mode: pdfFile.startsWith(\"blob:\") ? \"full-fetch\" : \"auto\",\n      })\n      .wait((response) => {\n        response.task.wait((openedDocument) => {\n          onDocumentLoadSuccessRef.current?.(openedDocument.pageCount)\n          previousDocumentIds.forEach((documentIdToClose) => {\n            documentManager.closeDocument(documentIdToClose).wait(\n              () => undefined,\n              () => undefined\n            )\n          })\n        }, handleOpenError)\n      }, handleOpenError)\n  }, [documentManager, pdfFile])\n  const document =\n    activeDocument?.status === \"loaded\" ? activeDocument.document : null\n  const documentFailed = loadError || activeDocument?.status === \"error\"\n  if (!activeDocumentId || documentFailed || !pdfFile) {\n    return (\n      <PDFViewerFallbackShell\n        className={innerProps.className}\n        defaultZoom={innerProps.defaultZoom}\n        showDownload={innerProps.showDownload}\n        showRotateControls={innerProps.showRotateControls}\n        showToolbar={innerProps.showToolbar}\n        showUpload={innerProps.showUpload}\n        sidebarOpen={false}\n        state={!pdfFile ? \"empty\" : documentFailed ? \"error\" : \"loading\"}\n        toolbarActions={innerProps.toolbarActions}\n        onUploadFile={(file) => {\n          innerProps.onUploadFile(file)\n          innerProps.onPdfUpload?.(file)\n        }}\n      />\n    )\n  }\n  return (\n    <PDFViewerInner\n      key={activeDocumentId}\n      {...innerProps}\n      pdfFile={pdfFile}\n      documentId={activeDocumentId}\n      document={document}\n    />\n  )\n}\nexport const PDFViewer = React.forwardRef<PDFViewerHandle, PDFViewerProps>(\n  function PDFViewer(\n    {\n      className,\n      defaultZoom = DEFAULT_ZOOM,\n      fileName,\n      resolveScrollAreaViewport,\n      showDownload = true,\n      showRotateControls = true,\n      showToolbar = true,\n      showUpload = true,\n      src,\n      toolbarActions,\n      pageClassName,\n      renderPageOverlay,\n      onActivePageChange,\n      onDocumentLoadSuccess,\n      onPdfUpload,\n      onPagePointerDown,\n      onPagePointerMove,\n      onPagePointerUp,\n      onPagePointerCancel,\n    },\n    ref\n  ) {\n    const { engine, error: engineError } = useSharedPdfEngine()\n    const [uploadedPdfFile, setUploadedPdfFile] = React.useState<{\n      src: string | undefined\n      url: string | null\n    }>(() => ({ src, url: null }))\n    const uploadedPdfUrl =\n      uploadedPdfFile.src === src ? uploadedPdfFile.url : null\n    const pdfFile = uploadedPdfUrl ?? src ?? \"\"\n    React.useEffect(\n      () => () => {\n        if (uploadedPdfUrl) URL.revokeObjectURL(uploadedPdfUrl)\n      },\n      [uploadedPdfUrl]\n    )\n    const handleUploadFile = React.useCallback(\n      (nextFile: File) => {\n        const nextUrl = URL.createObjectURL(nextFile)\n        setUploadedPdfFile({ src, url: nextUrl })\n      },\n      [src]\n    )\n    // Plugin registrations are created once per viewer instance.\n    const [plugins] = React.useState(() => [\n      createPluginRegistration(DocumentManagerPluginPackage),\n      createPluginRegistration(ViewportPluginPackage, {\n        viewportGap: PAGE_GAP,\n      }),\n      createPluginRegistration(ScrollPluginPackage, {\n        defaultPageGap: PAGE_GAP,\n        defaultBufferSize: 2,\n      }),\n      createPluginRegistration(RenderPluginPackage),\n      createPluginRegistration(TilingPluginPackage, {\n        tileSize: 768,\n        overlapPx: 2.5,\n        extraRings: 0,\n      }),\n      createPluginRegistration(InteractionManagerPluginPackage),\n      createPluginRegistration(SelectionPluginPackage, {\n        marquee: { enabled: false },\n      }),\n      createPluginRegistration(SearchPluginPackage, {\n        showAllResults: true,\n      }),\n      createPluginRegistration(ThumbnailPluginPackage, {\n        width: THUMBNAIL_WIDTH,\n        gap: THUMBNAIL_GAP,\n        imagePadding: THUMBNAIL_IMAGE_PADDING,\n        labelHeight: THUMBNAIL_LABEL_HEIGHT,\n        paddingY: THUMBNAIL_PANE_PADDING_Y,\n        buffer: 3,\n        autoScroll: true,\n        scrollBehavior: \"auto\",\n      }),\n      createPluginRegistration(ZoomPluginPackage, {\n        defaultZoomLevel: defaultZoom,\n        minZoom: ZOOM_OPTIONS[0],\n        maxZoom: ZOOM_OPTIONS[ZOOM_OPTIONS.length - 1],\n      }),\n      createPluginRegistration(RotatePluginPackage),\n    ])\n    if (engineError) {\n      return (\n        <PDFViewerFallbackShell\n          className={className}\n          defaultZoom={defaultZoom}\n          errorMessage=\"Unable to load the PDF engine.\"\n          showDownload={showDownload}\n          showRotateControls={showRotateControls}\n          showToolbar={showToolbar}\n          showUpload={showUpload}\n          sidebarOpen={false}\n          state=\"error\"\n          toolbarActions={toolbarActions}\n          onUploadFile={(file) => {\n            handleUploadFile(file)\n            onPdfUpload?.(file)\n          }}\n        />\n      )\n    }\n    if (!engine) {\n      return (\n        <PDFViewerFallbackShell\n          className={className}\n          defaultZoom={defaultZoom}\n          showDownload={showDownload}\n          showRotateControls={showRotateControls}\n          showToolbar={showToolbar}\n          showUpload={showUpload}\n          sidebarOpen={false}\n          state=\"loading\"\n          toolbarActions={toolbarActions}\n          onUploadFile={(file) => {\n            handleUploadFile(file)\n            onPdfUpload?.(file)\n          }}\n        />\n      )\n    }\n    return (\n      <PDFViewerScrollAreaResolverContext.Provider\n        value={resolveScrollAreaViewport ?? resolveDefaultScrollAreaViewport}\n      >\n        <EmbedPDF engine={engine} plugins={plugins}>\n          <PDFViewerDocumentLoader\n            viewerRef={ref}\n            pdfFile={pdfFile}\n            defaultZoom={defaultZoom}\n            className={className}\n            fileName={fileName}\n            showDownload={showDownload}\n            showToolbar={showToolbar}\n            showRotateControls={showRotateControls}\n            showUpload={showUpload}\n            toolbarActions={toolbarActions}\n            pageClassName={pageClassName}\n            renderPageOverlay={renderPageOverlay}\n            onActivePageChange={onActivePageChange}\n            onDocumentLoadSuccess={onDocumentLoadSuccess}\n            onPdfUpload={onPdfUpload}\n            onPagePointerDown={onPagePointerDown}\n            onPagePointerMove={onPagePointerMove}\n            onPagePointerUp={onPagePointerUp}\n            onPagePointerCancel={onPagePointerCancel}\n            onUploadFile={handleUploadFile}\n          />\n        </EmbedPDF>\n      </PDFViewerScrollAreaResolverContext.Provider>\n    )\n  }\n)\nfunction InlineSpinner({ className, ...props }: InlineRegistryIconProps) {\n  return (\n    <IconPlaceholder\n      lucide=\"LoaderCircle\"\n      tabler=\"IconLoader2\"\n      hugeicons=\"Loading03Icon\"\n      phosphor=\"CircleNotchIcon\"\n      remixicon=\"RiLoader4Line\"\n      role=\"status\"\n      aria-label=\"Loading\"\n      className={cn(\"size-4 animate-spin\", className)}\n      {...props}\n    />\n  )\n}\ntype InlineRegistryIconProps = Omit<\n  React.ComponentProps<\"svg\">,\n  \"children\" | \"strokeWidth\"\n> & { strokeWidth?: number }\n",
      "type": "registry:ui",
      "target": "@components/extend/pdf-viewer.tsx"
    },
    {
      "path": "lib/pdf-thumbnail-utils.ts",
      "content": "import type { PdfDocumentObject, PdfEngine } from \"@embedpdf/models\"\n\nconst PDFIUM_VERSION = \"2.15.0\"\nconst PDFIUM_WASM_URL = `https://cdn.jsdelivr.net/npm/@embedpdf/pdfium@${PDFIUM_VERSION}/dist/pdfium.wasm`\n\nlet sharedEnginePromise: Promise<PdfEngine> | null = null\nconst pdfDocumentCache = new Map<string, Promise<PdfDocumentObject>>()\nconst thumbnailUrlCache = new Map<string, Promise<string | null>>()\n\nexport function loadSharedPdfEngine() {\n  sharedEnginePromise ??= import(\"@embedpdf/engines/pdfium-worker-engine\").then(\n    ({ createPdfiumEngine }) => createPdfiumEngine(PDFIUM_WASM_URL, {})\n  )\n\n  return sharedEnginePromise\n}\n\nexport async function loadPdfDocument(url: string) {\n  let documentPromise = pdfDocumentCache.get(url)\n\n  if (!documentPromise) {\n    documentPromise = loadSharedPdfEngine().then((engine) =>\n      engine\n        .openDocumentUrl(\n          { id: url, url },\n          { mode: url.startsWith(\"blob:\") ? \"full-fetch\" : \"auto\" }\n        )\n        .toPromise()\n    )\n    pdfDocumentCache.set(url, documentPromise)\n  }\n\n  return documentPromise\n}\n\nexport async function getPdfPageCount(url: string) {\n  return (await loadPdfDocument(url)).pageCount\n}\n\nexport function renderPdfThumbnailUrl({\n  dpr = typeof window === \"undefined\" ? 1 : window.devicePixelRatio || 1,\n  pageIndex,\n  url,\n  width,\n}: {\n  dpr?: number\n  pageIndex: number\n  url: string\n  width: number\n}) {\n  const cacheKey = `${url}#${pageIndex}@${width}x${dpr}`\n  let thumbnailPromise = thumbnailUrlCache.get(cacheKey)\n\n  if (!thumbnailPromise) {\n    thumbnailPromise = (async () => {\n      const [engine, document] = await Promise.all([\n        loadSharedPdfEngine(),\n        loadPdfDocument(url),\n      ])\n      const page = document.pages[pageIndex]\n\n      if (!page) return null\n\n      const blob = await engine\n        .renderThumbnail(document, page, {\n          dpr,\n          imageType: \"image/png\",\n          scaleFactor: width / page.size.width,\n          withAnnotations: true,\n        })\n        .toPromise()\n\n      return URL.createObjectURL(blob)\n    })()\n    thumbnailUrlCache.set(cacheKey, thumbnailPromise)\n  }\n\n  return thumbnailPromise\n}\n",
      "type": "registry:lib",
      "target": "@lib/pdf-thumbnail-utils.ts"
    }
  ],
  "categories": [
    "documents"
  ],
  "type": "registry:ui"
}
