{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdf-viewer",
  "title": "PDF Viewer",
  "author": "ilinxa",
  "description": "Drop-in PDF reader — toolbar, zoom, selectable text, drag-drop, and a themed context menu. No commercial SDK.",
  "dependencies": [
    "react-pdf",
    "pdfjs-dist@5.4.296",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "context-menu",
    "dialog",
    "dropdown-menu",
    "input",
    "separator",
    "skeleton",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/registry/components/media/pdf-viewer/pdf-viewer.tsx",
      "content": "\"use client\";\n\nimport \"react-pdf/dist/Page/AnnotationLayer.css\";\nimport \"react-pdf/dist/Page/TextLayer.css\";\n\nimport {\n  forwardRef,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Document } from \"react-pdf\";\nimport type { PDFDocumentProxy } from \"pdfjs-dist\";\nimport { cn } from \"@/lib/utils\";\n\nimport { PdfViewerContext } from \"./hooks/use-pdf-viewer-context\";\nimport { usePdfDocument } from \"./hooks/use-pdf-document\";\nimport { usePdfDrop } from \"./hooks/use-pdf-drop\";\nimport { usePdfKeyboard } from \"./hooks/use-pdf-keyboard\";\nimport { usePdfPageTracker } from \"./hooks/use-pdf-page-tracker\";\nimport { usePdfPrint } from \"./hooks/use-pdf-print\";\nimport { usePdfSelection } from \"./hooks/use-pdf-selection\";\nimport { usePdfZoom } from \"./hooks/use-pdf-zoom\";\nimport { resolveVisiblePages } from \"./hooks/use-pdf-virtualization\";\n\nimport { PdfContextMenu } from \"./parts/pdf-context-menu\";\nimport { PdfDropOverlay } from \"./parts/pdf-drop-overlay\";\nimport { PdfEmptyState } from \"./parts/pdf-empty-state\";\nimport { PdfErrorState } from \"./parts/pdf-error-state\";\nimport { PdfLoadingState } from \"./parts/pdf-loading-state\";\nimport { PdfPage } from \"./parts/pdf-page\";\nimport { PdfPasswordPrompt } from \"./parts/pdf-password-prompt\";\nimport { PdfToolbar } from \"./parts/pdf-toolbar\";\n\nimport { downloadAsFile } from \"./lib/download\";\nimport { getSourceFilename, normalizeSource } from \"./lib/normalize-source\";\nimport { ensureWorkerConfigured } from \"./lib/worker-config\";\n\nimport {\n  DEFAULT_PDF_VIEWER_LABELS,\n  type PdfActions,\n  type PdfDocumentState,\n  type PdfFitMode,\n  type PdfRotation,\n  type PdfSource,\n  type PdfStatus,\n  type PdfViewerHandle,\n  type PdfViewerLabels,\n  type PdfViewerProps,\n  type ResolvedPdfViewerLabels,\n} from \"./types\";\n\nconst COMPACT_BREAKPOINT_PX = 480;\n\nfunction resolveLabels(\n  override: PdfViewerLabels | undefined,\n): ResolvedPdfViewerLabels {\n  if (!override) return DEFAULT_PDF_VIEWER_LABELS;\n  return { ...DEFAULT_PDF_VIEWER_LABELS, ...override };\n}\n\nexport const PdfViewer = forwardRef<PdfViewerHandle, PdfViewerProps>(\n  function PdfViewer(props, ref) {\n    const {\n      source,\n      toolbar = true,\n      renderToolbar,\n      enableDragDrop = true,\n      dragDropOverlay = true,\n      initialPage = 1,\n      initialScale = \"fit-width\",\n      initialRotation = 0,\n      virtualize = \"auto\",\n      virtualizeThreshold = 50,\n      enableContextMenu = true,\n      renderContextMenu,\n      onSearchSelection,\n      password,\n      renderPasswordPrompt,\n      workerSrc,\n      allowDownload = true,\n      allowPrint = true,\n      downloadFilename = \"document.pdf\",\n      onLoad,\n      onError,\n      onPageChange,\n      onScaleChange,\n      onRotationChange,\n      onSelection,\n      onSourceChange,\n      className,\n      style,\n      scrollContainerClassName,\n      pageClassName,\n      labels: labelsProp,\n      ariaLabel,\n    } = props;\n\n    const labels = useMemo(() => resolveLabels(labelsProp), [labelsProp]);\n\n    // Worker config\n    useEffect(() => {\n      ensureWorkerConfigured(workerSrc);\n    }, [workerSrc]);\n\n    // Internal source state — driven by `source` prop, mutated by drop.\n    const [internalSource, setInternalSource] = useState<PdfSource | null>(\n      source ?? null,\n    );\n    const lastPropSourceRef = useRef<PdfSource | null | undefined>(source);\n    useEffect(() => {\n      if (lastPropSourceRef.current !== source) {\n        lastPropSourceRef.current = source;\n        setInternalSource(source ?? null);\n        onSourceChange?.({ source: source ?? null, reason: \"prop\" });\n      }\n    }, [source, onSourceChange]);\n\n    // Notify on dropped-source change (when user drops a file)\n    const replaceSource = (\n      next: PdfSource | null,\n      reason: \"prop\" | \"drop\",\n    ) => {\n      setInternalSource(next);\n      onSourceChange?.({ source: next, reason });\n    };\n\n    const normalizedSource = useMemo(\n      () => normalizeSource(internalSource),\n      [internalSource],\n    );\n\n    // Document loading lifecycle\n    const documentState = usePdfDocument({\n      source: normalizedSource,\n      password,\n    });\n\n    // Page sizes for virtualization placeholders + fit-scale.\n    const [pageSizes, setPageSizes] = useState<\n      Array<{ width: number; height: number } | null>\n    >([]);\n    useEffect(() => {\n      const pdf = documentState.pdfDocument;\n      if (!pdf) {\n        setPageSizes([]);\n        return;\n      }\n      let cancelled = false;\n      const N = pdf.numPages;\n      const sizes: Array<{ width: number; height: number } | null> = new Array(\n        N,\n      ).fill(null);\n\n      (async () => {\n        const concurrency = 8;\n        for (let start = 0; start < N; start += concurrency) {\n          if (cancelled) return;\n          const batch: Promise<void>[] = [];\n          for (let i = start; i < Math.min(start + concurrency, N); i++) {\n            batch.push(\n              pdf.getPage(i + 1).then((page) => {\n                if (cancelled) return;\n                const vp = page.getViewport({ scale: 1, rotation: 0 });\n                sizes[i] = { width: vp.width, height: vp.height };\n              }),\n            );\n          }\n          await Promise.all(batch);\n          if (!cancelled) setPageSizes([...sizes]);\n        }\n      })();\n\n      return () => {\n        cancelled = true;\n      };\n    }, [documentState.pdfDocument]);\n\n    // Refs\n    const rootRef = useRef<HTMLElement>(null);\n    const scrollContainerRef = useRef<HTMLDivElement>(null);\n\n    // Compact mode tracking via ResizeObserver\n    const [compact, setCompact] = useState(false);\n    /*\n     * v0.3.1 — open-state + anchor point for the `renderContextMenu` slot.\n     * `null` means closed; the built-in Radix menu does not use this (it owns\n     * its own positioning), so this is purely the consumer-slot path.\n     */\n    const [customMenuPos, setCustomMenuPos] = useState<{\n      x: number;\n      y: number;\n    } | null>(null);\n\n    /*\n     * Escape closes the custom context menu.\n     *\n     * Review finding: this started as `onKeyDown` on the scroll container, which\n     * only fires while THAT element has focus — and a context menu almost always\n     * takes focus itself the moment it opens, so the handler could not fire in\n     * the one situation it existed for. Listening at the document is what makes\n     * the documented behaviour actually true.\n     */\n    useEffect(() => {\n      if (!customMenuPos) return;\n      const onKey = (event: KeyboardEvent) => {\n        if (event.key === \"Escape\") setCustomMenuPos(null);\n      };\n      document.addEventListener(\"keydown\", onKey);\n      return () => document.removeEventListener(\"keydown\", onKey);\n    }, [customMenuPos]);\n    useEffect(() => {\n      const root = rootRef.current;\n      if (!root) return;\n      const ro = new ResizeObserver(() => {\n        setCompact(root.clientWidth < COMPACT_BREAKPOINT_PX);\n      });\n      ro.observe(root);\n      setCompact(root.clientWidth < COMPACT_BREAKPOINT_PX);\n      return () => ro.disconnect();\n    }, []);\n\n    // Page state\n    const [page, setPage] = useState<number>(Math.max(1, initialPage));\n    const [rotation, setRotation] = useState<PdfRotation>(initialRotation);\n\n    // Reset page + rotation on document change\n    useEffect(() => {\n      setPage(Math.max(1, initialPage));\n      setRotation(initialRotation);\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [documentState.pdfDocument]);\n\n    // Base page dimensions for fit-scale (use first page)\n    const basePage = pageSizes[0] ?? null;\n\n    // Zoom hook\n    const zoom = usePdfZoom({\n      containerRef: scrollContainerRef,\n      basePageWidth: basePage?.width ?? 612,\n      basePageHeight: basePage?.height ?? 792,\n      rotation,\n      initialScale,\n      onScaleChange,\n    });\n\n    // Page tracker\n    const pageTracker = usePdfPageTracker({\n      containerRef: scrollContainerRef,\n      numPages: documentState.numPages,\n      ready: documentState.status === \"ready\",\n      onPageChange: (next) => {\n        setPage((prev) => {\n          if (prev !== next) {\n            onPageChange?.({ page: next, source: internalSource });\n          }\n          return next;\n        });\n      },\n    });\n\n    // Print\n    const printer = usePdfPrint({\n      pdfDocument: documentState.pdfDocument,\n      rotation,\n    });\n\n    // Selection\n    const selectedText = usePdfSelection({\n      rootRef: rootRef as unknown as { current: HTMLElement | null },\n      onSelection,\n    });\n\n    // Drop\n    const drop = usePdfDrop({\n      enabled: enableDragDrop,\n      onPdfDropped: (file) => {\n        replaceSource(file, \"drop\");\n      },\n      onNonPdfDropped: () => {\n        // Hook into a future toast — for now, no-op (label is exposed via ctx).\n      },\n    });\n\n    // Status (drives UI surface)\n    const status: PdfStatus = useMemo(() => {\n      if (documentState.status === \"idle\") return \"empty\";\n      if (documentState.status === \"loading\") return \"loading\";\n      if (documentState.status === \"password\") return \"password\";\n      if (documentState.status === \"ready\") return \"ready\";\n      return \"error\";\n    }, [documentState.status]);\n\n    // Notify load + error\n    const lastLoadedRef = useRef<PDFDocumentProxy | null>(null);\n    useEffect(() => {\n      if (\n        documentState.status === \"ready\" &&\n        documentState.pdfDocument &&\n        lastLoadedRef.current !== documentState.pdfDocument\n      ) {\n        lastLoadedRef.current = documentState.pdfDocument;\n        onLoad?.({\n          numPages: documentState.numPages,\n          pdfDocument: documentState.pdfDocument,\n        });\n      }\n      if (documentState.status === \"error\" && documentState.error) {\n        onError?.({\n          error: documentState.error,\n          source: internalSource,\n        });\n      }\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [documentState.status, documentState.pdfDocument, documentState.error]);\n\n    // Notify rotation change\n    useEffect(() => {\n      onRotationChange?.({ rotation });\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [rotation]);\n\n    // Build actions\n    const actions = useMemo<PdfActions>(\n      () => ({\n        goToPage: (n: number) => {\n          const total = documentState.numPages;\n          const clamped = Math.max(\n            1,\n            Math.min(total > 0 ? total : n, Math.floor(n)),\n          );\n          pageTracker.scrollToPage(clamped);\n        },\n        goToNextPage: () => {\n          const total = documentState.numPages;\n          if (page < total) pageTracker.scrollToPage(page + 1);\n        },\n        goToPrevPage: () => {\n          if (page > 1) pageTracker.scrollToPage(page - 1);\n        },\n        setScale: (s) => zoom.setScale(s),\n        zoomIn: () => zoom.zoomIn(),\n        zoomOut: () => zoom.zoomOut(),\n        resetZoom: () => zoom.resetZoom(),\n        rotate: (delta = 90) => {\n          setRotation((current) => {\n            const next = (((current + delta) % 360) + 360) % 360;\n            const snapped = (Math.round(next / 90) * 90) % 360;\n            return snapped as PdfRotation;\n          });\n        },\n        setRotation: (r) => setRotation(r),\n        download: () => {\n          const filename =\n            getSourceFilename(internalSource) ?? downloadFilename;\n          void downloadAsFile(internalSource, filename);\n        },\n        print: () => {\n          if (!allowPrint) return;\n          void printer.print();\n        },\n      }),\n      [\n        documentState.numPages,\n        page,\n        zoom,\n        pageTracker,\n        internalSource,\n        downloadFilename,\n        allowPrint,\n        printer,\n      ],\n    );\n\n    // Keyboard\n    usePdfKeyboard({\n      containerRef: scrollContainerRef,\n      rootRef: rootRef as unknown as { current: HTMLElement | null },\n      actions,\n      status,\n      allowDownload,\n      allowPrint,\n    });\n\n    // Document state for context\n    const docState: PdfDocumentState = useMemo(\n      () => ({\n        page,\n        numPages: documentState.numPages,\n        scale: zoom.scale,\n        fitMode: zoom.fitMode as PdfFitMode,\n        rotation,\n        loading: status === \"loading\",\n        ready: status === \"ready\",\n        error: documentState.error,\n        source: internalSource,\n        selectedText,\n        status,\n      }),\n      [\n        page,\n        documentState.numPages,\n        zoom.scale,\n        zoom.fitMode,\n        rotation,\n        status,\n        documentState.error,\n        internalSource,\n        selectedText,\n      ],\n    );\n\n    // Imperative handle\n    useImperativeHandle(\n      ref,\n      () => ({\n        actions,\n        state: docState,\n        pdfDocument: documentState.pdfDocument,\n      }),\n      [actions, docState, documentState.pdfDocument],\n    );\n\n    // Context value\n    const contextValue = {\n      ...docState,\n      actions,\n      labels,\n      allowDownload,\n      allowPrint,\n      compact,\n      pdfDocument: documentState.pdfDocument,\n    };\n\n    // Visible pages for rendering\n    const visiblePages = useMemo(\n      () =>\n        resolveVisiblePages({\n          numPages: documentState.numPages,\n          currentPage: page,\n          mode: virtualize,\n          threshold: virtualizeThreshold,\n        }),\n      [documentState.numPages, page, virtualize, virtualizeThreshold],\n    );\n\n    // Toolbar slot context\n    const toolbarCtx = {\n      ...docState,\n      actions,\n      allowDownload,\n      allowPrint,\n      compact,\n    };\n\n    // Password prompt slot context\n    const passwordCtx = {\n      submit: documentState.submitPassword,\n      cancel: documentState.cancelPassword,\n      error: documentState.error,\n      attempts: documentState.passwordAttempts,\n    };\n\n    // Context menu slot context\n    const contextMenuCtx = {\n      ...docState,\n      actions,\n      /*\n       * v0.3.1 — real position + a close that closes.\n       *\n       * The custom slot used to render UNCONDITIONALLY at `{x:0, y:0}` with an\n       * empty-bodied `closeMenu`, which made it unusable as a context menu: it\n       * was always on screen, always in the corner, and could not be dismissed.\n       * The built-in menu never hit this because Radix owns its own positioning\n       * — only the consumer-facing slot was broken.\n       */\n      position: customMenuPos ?? { x: 0, y: 0 },\n      closeMenu: () => setCustomMenuPos(null),\n      onSearch: onSearchSelection\n        ? (text: string) => onSearchSelection({ text })\n        : null,\n    };\n\n    return (\n      <PdfViewerContext.Provider value={contextValue}>\n        <section\n          ref={rootRef as React.RefObject<HTMLElement>}\n          aria-label={ariaLabel ?? labels.viewerAriaLabel}\n          className={cn(\n            \"relative flex h-full w-full flex-col overflow-hidden rounded-lg border border-border bg-background\",\n            className,\n          )}\n          style={style}\n          data-pdf-viewer-root\n        >\n          {toolbar\n            ? (renderToolbar?.(toolbarCtx) ?? <PdfToolbar />)\n            : null}\n\n          <div\n            ref={scrollContainerRef}\n            {...(enableDragDrop ? drop.dropProps : {})}\n            onContextMenu={\n              enableContextMenu && renderContextMenu\n                ? (event) => {\n                    event.preventDefault();\n                    setCustomMenuPos({ x: event.clientX, y: event.clientY });\n                  }\n                : undefined\n            }\n            className={cn(\n              \"relative flex-1 overflow-auto bg-muted/30 [touch-action:pan-x_pan-y]\",\n              \"[&_.react-pdf__Page__textContent]:select-text\",\n              scrollContainerClassName,\n            )}\n            tabIndex={0}\n          >\n            {status === \"empty\" ? (\n              <PdfEmptyState\n                labels={labels}\n                enableDragDrop={enableDragDrop}\n              />\n            ) : null}\n            {status === \"loading\" ? <PdfLoadingState labels={labels} /> : null}\n            {status === \"error\" && documentState.error ? (\n              <PdfErrorState\n                labels={labels}\n                error={documentState.error}\n                onRetry={documentState.retry}\n              />\n            ) : null}\n            {/*\n              Document wraps the page list so AnnotationLayer/TextLayer can\n              read `pdf` from DocumentContext. `className=\"contents\"` keeps\n              its wrapper div transparent to layout.\n            */}\n            {normalizedSource ? (\n              <Document\n                file={normalizedSource}\n                loading={null}\n                error={null}\n                noData={null}\n                externalLinkTarget=\"_blank\"\n                className=\"contents\"\n                {...documentState.documentCallbacks}\n              >\n                {status === \"ready\" && documentState.pdfDocument ? (\n                  <PdfContextMenu\n                    enabled={enableContextMenu && !renderContextMenu}\n                    onSearchSelection={\n                      onSearchSelection\n                        ? (text) => onSearchSelection({ text })\n                        : undefined\n                    }\n                    className=\"flex flex-col items-center gap-3 p-4\"\n                  >\n                    {Array.from(\n                      { length: documentState.numPages },\n                      (_, i) => {\n                        const pageNumber = i + 1;\n                        const isVisible = visiblePages.has(pageNumber);\n                        return (\n                          <PdfPage\n                            key={pageNumber}\n                            pdfDocument={documentState.pdfDocument!}\n                            pageNumber={pageNumber}\n                            scale={zoom.scale}\n                            rotation={rotation}\n                            basePageSize={pageSizes[i] ?? null}\n                            placeholder={!isVisible}\n                            className={pageClassName}\n                          />\n                        );\n                      },\n                    )}\n                  </PdfContextMenu>\n                ) : null}\n              </Document>\n            ) : null}\n          </div>\n\n          {status === \"password\"\n            ? (renderPasswordPrompt?.(passwordCtx) ?? (\n                <PdfPasswordPrompt\n                  open={true}\n                  labels={labels}\n                  error={documentState.error}\n                  attempts={documentState.passwordAttempts}\n                  onSubmit={documentState.submitPassword}\n                  onCancel={documentState.cancelPassword}\n                />\n              ))\n            : null}\n\n          {/* Custom context-menu slot (when consumer overrides) */}\n          {status === \"ready\" && enableContextMenu && renderContextMenu && customMenuPos\n            ? renderContextMenu(contextMenuCtx)\n            : null}\n\n          {drop.isDragging && enableDragDrop && dragDropOverlay ? (\n            <PdfDropOverlay labels={labels} />\n          ) : null}\n        </section>\n      </PdfViewerContext.Provider>\n    );\n  },\n);\n\nexport default PdfViewer;\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/pdf-viewer.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/index.ts",
      "content": "export { PdfViewer, default } from \"./pdf-viewer\";\n\n// Toolbar parts (reads viewer context internally)\nexport { PdfToolbar } from \"./parts/pdf-toolbar\";\nexport { PdfPageNav } from \"./parts/pdf-page-nav\";\nexport { PdfPageIndicator } from \"./parts/pdf-page-indicator\";\nexport { PdfZoomControls } from \"./parts/pdf-zoom-controls\";\nexport { PdfActionMenu } from \"./parts/pdf-action-menu\";\n\n// Public hook for advanced consumers building bespoke chrome\nexport { usePdfViewer } from \"./hooks/use-pdf-viewer-context\";\n\n// Defaults\nexport { DEFAULT_PDF_VIEWER_LABELS } from \"./types\";\n\nexport type {\n  PdfSource,\n  PdfInitialScale,\n  PdfRotation,\n  PdfFitMode,\n  PdfVirtualizeMode,\n  PdfStatus,\n  PdfActions,\n  PdfDocumentState,\n  PdfToolbarContext,\n  PdfContextMenuContext,\n  PdfPasswordPromptContext,\n  PdfViewerHandle,\n  PdfViewerLabels,\n  PdfViewerProps,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/index.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/types.ts",
      "content": "import type { CSSProperties, ReactNode } from \"react\";\nimport type { PDFDocumentProxy } from \"pdfjs-dist\";\n\nexport type PdfSource = string | File | Blob | ArrayBuffer | Uint8Array;\n\nexport type PdfInitialScale = \"fit-width\" | \"fit-page\" | number;\n\nexport type PdfRotation = 0 | 90 | 180 | 270;\n\nexport type PdfFitMode = \"fit-width\" | \"fit-page\" | null;\n\nexport type PdfVirtualizeMode = \"auto\" | \"always\" | \"never\";\n\nexport type PdfStatus =\n  | \"empty\"\n  | \"loading\"\n  | \"password\"\n  | \"ready\"\n  | \"error\";\n\nexport interface PdfActions {\n  goToPage: (page: number) => void;\n  goToNextPage: () => void;\n  goToPrevPage: () => void;\n  setScale: (scale: number | \"fit-width\" | \"fit-page\") => void;\n  zoomIn: () => void;\n  zoomOut: () => void;\n  resetZoom: () => void;\n  rotate: (deltaDegrees?: number) => void;\n  setRotation: (rotation: PdfRotation) => void;\n  download: () => void;\n  print: () => void;\n}\n\nexport interface PdfDocumentState {\n  page: number;\n  numPages: number;\n  scale: number;\n  fitMode: PdfFitMode;\n  rotation: PdfRotation;\n  loading: boolean;\n  ready: boolean;\n  error: Error | null;\n  source: PdfSource | null;\n  selectedText: string;\n  status: PdfStatus;\n}\n\nexport interface PdfToolbarContext extends PdfDocumentState {\n  actions: PdfActions;\n  allowDownload: boolean;\n  allowPrint: boolean;\n  compact: boolean;\n}\n\nexport interface PdfContextMenuContext extends PdfDocumentState {\n  actions: PdfActions;\n  position: { x: number; y: number };\n  closeMenu: () => void;\n  onSearch: ((text: string) => void) | null;\n}\n\nexport interface PdfPasswordPromptContext {\n  submit: (password: string) => void;\n  cancel: () => void;\n  error: Error | null;\n  attempts: number;\n}\n\nexport interface PdfViewerHandle {\n  actions: PdfActions;\n  state: PdfDocumentState;\n  pdfDocument: PDFDocumentProxy | null;\n}\n\nexport interface PdfViewerLabels {\n  prevPage?: ReactNode;\n  nextPage?: ReactNode;\n  zoomIn?: ReactNode;\n  zoomOut?: ReactNode;\n  fitWidth?: ReactNode;\n  fitPage?: ReactNode;\n  resetZoom?: ReactNode;\n  rotate?: ReactNode;\n  download?: ReactNode;\n  print?: ReactNode;\n  pageOfTotal?: (page: number, total: number) => string;\n  scalePercent?: (scale: number) => string;\n  more?: ReactNode;\n  jumpToPage?: ReactNode;\n\n  loading?: ReactNode;\n  emptyTitle?: ReactNode;\n  emptyHint?: ReactNode;\n  errorTitle?: ReactNode;\n  errorRetry?: ReactNode;\n\n  dropPdfHere?: ReactNode;\n  notAPdfFile?: ReactNode;\n\n  passwordTitle?: ReactNode;\n  passwordHint?: ReactNode;\n  passwordPlaceholder?: string;\n  passwordSubmit?: ReactNode;\n  passwordCancel?: ReactNode;\n  passwordError?: ReactNode;\n\n  copyText?: ReactNode;\n  searchSelection?: ReactNode;\n  contextNextPage?: ReactNode;\n  contextPrevPage?: ReactNode;\n  contextZoomIn?: ReactNode;\n  contextZoomOut?: ReactNode;\n  contextRotate?: ReactNode;\n  contextDownload?: ReactNode;\n  contextPrint?: ReactNode;\n\n  toolbarAriaLabel?: string;\n  viewerAriaLabel?: string;\n}\n\nexport type ResolvedPdfViewerLabels = Omit<\n  Required<PdfViewerLabels>,\n  \"pageOfTotal\" | \"scalePercent\"\n> & {\n  pageOfTotal: (page: number, total: number) => string;\n  scalePercent: (scale: number) => string;\n};\n\nexport const DEFAULT_PDF_VIEWER_LABELS: ResolvedPdfViewerLabels = {\n  prevPage: \"Previous page\",\n  nextPage: \"Next page\",\n  zoomIn: \"Zoom in\",\n  zoomOut: \"Zoom out\",\n  fitWidth: \"Fit width\",\n  fitPage: \"Fit page\",\n  resetZoom: \"Reset zoom\",\n  rotate: \"Rotate\",\n  download: \"Download\",\n  print: \"Print\",\n  pageOfTotal: (page, total) => `Page ${page} of ${total}`,\n  scalePercent: (scale) => `${Math.round(scale * 100)}%`,\n  more: \"More actions\",\n  jumpToPage: \"Jump to page\",\n\n  loading: \"Loading PDF…\",\n  emptyTitle: \"No document loaded\",\n  emptyHint: \"Drop a PDF here, or pass a `source` prop.\",\n  errorTitle: \"Couldn't load this PDF\",\n  errorRetry: \"Try again\",\n\n  dropPdfHere: \"Drop PDF here\",\n  notAPdfFile: \"Only PDF files are supported.\",\n\n  passwordTitle: \"This PDF is password-protected\",\n  passwordHint: \"Enter the password to view it.\",\n  passwordPlaceholder: \"Password\",\n  passwordSubmit: \"Unlock\",\n  passwordCancel: \"Cancel\",\n  passwordError: \"Wrong password. Try again.\",\n\n  copyText: \"Copy text\",\n  searchSelection: \"Search selection\",\n  contextNextPage: \"Next page\",\n  contextPrevPage: \"Previous page\",\n  contextZoomIn: \"Zoom in\",\n  contextZoomOut: \"Zoom out\",\n  contextRotate: \"Rotate\",\n  contextDownload: \"Download\",\n  contextPrint: \"Print\",\n\n  toolbarAriaLabel: \"PDF viewer controls\",\n  viewerAriaLabel: \"PDF document viewer\",\n};\n\nexport interface PdfViewerProps {\n  source?: PdfSource | null;\n\n  toolbar?: boolean;\n  renderToolbar?: (ctx: PdfToolbarContext) => ReactNode;\n\n  enableDragDrop?: boolean;\n  dragDropOverlay?: boolean;\n\n  initialPage?: number;\n  initialScale?: PdfInitialScale;\n  initialRotation?: PdfRotation;\n\n  virtualize?: PdfVirtualizeMode;\n  virtualizeThreshold?: number;\n\n  enableContextMenu?: boolean;\n  renderContextMenu?: (ctx: PdfContextMenuContext) => ReactNode;\n  onSearchSelection?: (args: { text: string }) => void;\n\n  password?: string;\n  renderPasswordPrompt?: (ctx: PdfPasswordPromptContext) => ReactNode;\n\n  workerSrc?: string;\n\n  allowDownload?: boolean;\n  allowPrint?: boolean;\n  downloadFilename?: string;\n\n  onLoad?: (args: { numPages: number; pdfDocument: PDFDocumentProxy }) => void;\n  onError?: (args: { error: Error; source: PdfSource | null }) => void;\n  onPageChange?: (args: { page: number; source: PdfSource | null }) => void;\n  onScaleChange?: (args: { scale: number; fitMode: PdfFitMode }) => void;\n  onRotationChange?: (args: { rotation: PdfRotation }) => void;\n  onSelection?: (args: { text: string }) => void;\n  onSourceChange?: (args: {\n    source: PdfSource | null;\n    reason: \"prop\" | \"drop\";\n  }) => void;\n\n  className?: string;\n  style?: CSSProperties;\n  scrollContainerClassName?: string;\n  pageClassName?: string;\n\n  labels?: PdfViewerLabels;\n\n  ariaLabel?: string;\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/types.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-viewer-context.ts",
      "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { PDFDocumentProxy } from \"pdfjs-dist\";\nimport type {\n  PdfActions,\n  PdfDocumentState,\n  ResolvedPdfViewerLabels,\n} from \"../types\";\n\nexport interface PdfViewerContextValue extends PdfDocumentState {\n  actions: PdfActions;\n  labels: ResolvedPdfViewerLabels;\n  allowDownload: boolean;\n  allowPrint: boolean;\n  compact: boolean;\n  pdfDocument: PDFDocumentProxy | null;\n}\n\nexport const PdfViewerContext = createContext<PdfViewerContextValue | null>(\n  null,\n);\n\nexport function usePdfViewer(): PdfViewerContextValue {\n  const ctx = useContext(PdfViewerContext);\n  if (ctx == null) {\n    throw new Error(\n      \"usePdfViewer must be used inside <PdfViewer>. \" +\n        \"Toolbar parts (PdfPageNav, PdfZoomControls, etc.) read state from \" +\n        \"the viewer context and only work as descendants of <PdfViewer>.\",\n    );\n  }\n  return ctx;\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-viewer-context.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-document.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { PDFDocumentProxy } from \"pdfjs-dist\";\nimport type { NormalizedPdfSource } from \"../lib/normalize-source\";\n\nexport type PdfDocumentStatus =\n  | \"idle\"\n  | \"loading\"\n  | \"password\"\n  | \"ready\"\n  | \"error\";\n\nexport interface UsePdfDocumentArgs {\n  source: NormalizedPdfSource | null;\n  password?: string;\n}\n\nexport interface PdfDocumentCallbacks {\n  onLoadSuccess: (pdf: PDFDocumentProxy) => void;\n  onLoadError: (error: Error) => void;\n  onSourceError: (error: Error) => void;\n  onPassword: (callback: (password: string) => void, reason: number) => void;\n}\n\nexport interface UsePdfDocumentReturn {\n  status: PdfDocumentStatus;\n  pdfDocument: PDFDocumentProxy | null;\n  error: Error | null;\n  numPages: number;\n  passwordAttempts: number;\n  documentCallbacks: PdfDocumentCallbacks;\n  submitPassword: (password: string) => void;\n  cancelPassword: () => void;\n  retry: () => void;\n}\n\n/**\n * Tracks the load lifecycle of a PDF document. The actual loading is done by\n * react-pdf's <Document> component (which natively handles all source shapes\n * — string URL / File / Blob / ArrayBuffer / { data } / { url } objects).\n * This hook returns callbacks to wire into <Document> and exposes the\n * resulting status as a discriminated state.\n */\nexport function usePdfDocument(args: UsePdfDocumentArgs): UsePdfDocumentReturn {\n  const { source, password } = args;\n\n  const [status, setStatus] = useState<PdfDocumentStatus>(\n    source == null ? \"idle\" : \"loading\",\n  );\n  const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy | null>(null);\n  const [error, setError] = useState<Error | null>(null);\n  const [passwordAttempts, setPasswordAttempts] = useState(0);\n  const [retryToken, setRetryToken] = useState(0);\n  const passwordCallbackRef = useRef<((password: string) => void) | null>(null);\n\n  // React 19 pattern — reset state when source or retryToken changes.\n  // setState during render is the recommended way to derive state from props.\n  // See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes\n  const [trackedKey, setTrackedKey] = useState<{\n    source: NormalizedPdfSource | null;\n    retryToken: number;\n  }>({ source, retryToken });\n  if (trackedKey.source !== source || trackedKey.retryToken !== retryToken) {\n    setTrackedKey({ source, retryToken });\n    setPdfDocument(null);\n    setError(null);\n    setPasswordAttempts(0);\n    setStatus(source == null ? \"idle\" : \"loading\");\n  }\n\n  // Clear the password callback ref when the tracked key changes — the old\n  // callback belongs to a stale loadingTask and should not be invoked.\n  useEffect(() => {\n    passwordCallbackRef.current = null;\n  }, [trackedKey]);\n\n  const handleLoadSuccess = useCallback((pdf: PDFDocumentProxy) => {\n    setPdfDocument(pdf);\n    setError(null);\n    setStatus(\"ready\");\n  }, []);\n\n  const handleLoadError = useCallback((err: Error) => {\n    setError(err);\n    setStatus(\"error\");\n  }, []);\n\n  const handleSourceError = useCallback((err: Error) => {\n    setError(err);\n    setStatus(\"error\");\n  }, []);\n\n  const handlePassword = useCallback(\n    (callback: (password: string) => void, reason: number) => {\n      passwordCallbackRef.current = callback;\n      // pdf.js PasswordResponses: NEED_PASSWORD = 1, INCORRECT_PASSWORD = 2\n      const isIncorrect = reason === 2;\n      if (password && !isIncorrect) {\n        callback(password);\n        passwordCallbackRef.current = null;\n        return;\n      }\n      if (isIncorrect) {\n        setPasswordAttempts((n) => n + 1);\n        setError(new Error(\"Incorrect password\"));\n      } else {\n        setError(null);\n      }\n      setStatus(\"password\");\n    },\n    [password],\n  );\n\n  const submitPassword = useCallback((p: string) => {\n    const cb = passwordCallbackRef.current;\n    if (cb) {\n      cb(p);\n      passwordCallbackRef.current = null;\n    }\n  }, []);\n\n  const cancelPassword = useCallback(() => {\n    passwordCallbackRef.current = null;\n    setError(new Error(\"Password required to view this PDF.\"));\n    setStatus(\"error\");\n  }, []);\n\n  const retry = useCallback(() => {\n    setRetryToken((n) => n + 1);\n  }, []);\n\n  return {\n    status,\n    pdfDocument,\n    error,\n    numPages: pdfDocument?.numPages ?? 0,\n    passwordAttempts,\n    documentCallbacks: {\n      onLoadSuccess: handleLoadSuccess,\n      onLoadError: handleLoadError,\n      onSourceError: handleSourceError,\n      onPassword: handlePassword,\n    },\n    submitPassword,\n    cancelPassword,\n    retry,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-document.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-zoom.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport {\n  PDF_VIEWER_MAX_SCALE,\n  PDF_VIEWER_MIN_SCALE,\n  clampScale,\n  nextZoomIn,\n  nextZoomOut,\n} from \"../lib/clamp-scale\";\nimport {\n  computeFitPageScale,\n  computeFitWidthScale,\n} from \"../lib/compute-fit-scale\";\nimport type { PdfFitMode, PdfRotation } from \"../types\";\n\nexport interface UsePdfZoomArgs {\n  containerRef: { current: HTMLElement | null };\n  basePageWidth: number;\n  basePageHeight: number;\n  rotation: PdfRotation;\n  initialScale: number | \"fit-width\" | \"fit-page\";\n  onScaleChange?: (args: { scale: number; fitMode: PdfFitMode }) => void;\n}\n\nexport interface UsePdfZoomReturn {\n  scale: number;\n  fitMode: PdfFitMode;\n  setScale: (next: number | \"fit-width\" | \"fit-page\") => void;\n  zoomIn: () => void;\n  zoomOut: () => void;\n  resetZoom: () => void;\n}\n\ninterface PendingZoomTarget {\n  cursorX: number;\n  cursorY: number;\n  docX: number;\n  docY: number;\n  oldScale: number;\n}\n\n/**\n * Manages numeric scale + fit-mode for the viewer. Handles ctrl/cmd + wheel\n * zoom (cursor-anchored) and 2-finger pinch zoom (midpoint-anchored).\n */\nexport function usePdfZoom(args: UsePdfZoomArgs): UsePdfZoomReturn {\n  const {\n    containerRef,\n    basePageWidth,\n    basePageHeight,\n    rotation,\n    initialScale,\n    onScaleChange,\n  } = args;\n\n  const [fitMode, setFitMode] = useState<PdfFitMode>(\n    initialScale === \"fit-width\" || initialScale === \"fit-page\"\n      ? initialScale\n      : null,\n  );\n  const [scale, setScaleState] = useState<number>(\n    typeof initialScale === \"number\" ? clampScale(initialScale) : 1,\n  );\n\n  const pendingTargetRef = useRef<PendingZoomTarget | null>(null);\n  const onScaleChangeRef = useRef(onScaleChange);\n  useEffect(() => {\n    onScaleChangeRef.current = onScaleChange;\n  }, [onScaleChange]);\n\n  const recomputeFitScale = useCallback(\n    (mode: \"fit-width\" | \"fit-page\"): number => {\n      const container = containerRef.current;\n      if (!container || basePageWidth <= 0 || basePageHeight <= 0) return 1;\n      const inputs = {\n        containerWidth: container.clientWidth,\n        containerHeight: container.clientHeight,\n        pageWidth: basePageWidth,\n        pageHeight: basePageHeight,\n        rotation,\n      };\n      return mode === \"fit-width\"\n        ? computeFitWidthScale(inputs)\n        : computeFitPageScale(inputs);\n    },\n    [containerRef, basePageWidth, basePageHeight, rotation],\n  );\n\n  // Apply fit-mode scale on mount, on rotation change, and on container resize.\n  useEffect(() => {\n    if (!fitMode) return;\n    const container = containerRef.current;\n    if (!container) return;\n\n    const apply = () => {\n      const next = recomputeFitScale(fitMode);\n      setScaleState(next);\n    };\n    apply();\n\n    const ro = new ResizeObserver(apply);\n    ro.observe(container);\n    return () => ro.disconnect();\n  }, [fitMode, recomputeFitScale, containerRef]);\n\n  // Notify parent when scale or fitMode actually changes.\n  const lastNotifiedRef = useRef<{ scale: number; fitMode: PdfFitMode }>({\n    scale,\n    fitMode,\n  });\n  useEffect(() => {\n    if (\n      lastNotifiedRef.current.scale !== scale ||\n      lastNotifiedRef.current.fitMode !== fitMode\n    ) {\n      lastNotifiedRef.current = { scale, fitMode };\n      onScaleChangeRef.current?.({ scale, fitMode });\n    }\n  }, [scale, fitMode]);\n\n  // After scale change from a cursor-anchored zoom, restore scroll so the\n  // cursor stays over the same point in the document.\n  useLayoutEffect(() => {\n    const target = pendingTargetRef.current;\n    if (!target) return;\n    pendingTargetRef.current = null;\n\n    const container = containerRef.current;\n    if (!container) return;\n    const factor = scale / target.oldScale;\n    container.scrollLeft = target.docX * factor - target.cursorX;\n    container.scrollTop = target.docY * factor - target.cursorY;\n  }, [scale, containerRef]);\n\n  const setScale = useCallback(\n    (next: number | \"fit-width\" | \"fit-page\") => {\n      if (next === \"fit-width\" || next === \"fit-page\") {\n        setFitMode(next);\n        // recompute via effect\n        return;\n      }\n      setFitMode(null);\n      setScaleState(clampScale(next));\n    },\n    [],\n  );\n\n  const zoomIn = useCallback(() => {\n    setFitMode(null);\n    setScaleState((s) => nextZoomIn(s));\n  }, []);\n  const zoomOut = useCallback(() => {\n    setFitMode(null);\n    setScaleState((s) => nextZoomOut(s));\n  }, []);\n  const resetZoom = useCallback(() => {\n    setFitMode(null);\n    setScaleState(1);\n  }, []);\n\n  // Wheel-zoom (Ctrl/Cmd + wheel). Cursor-anchored.\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const onWheel = (e: WheelEvent) => {\n      if (!(e.ctrlKey || e.metaKey)) return;\n      e.preventDefault();\n      const rect = container.getBoundingClientRect();\n      const cursorX = e.clientX - rect.left;\n      const cursorY = e.clientY - rect.top;\n      const docX = container.scrollLeft + cursorX;\n      const docY = container.scrollTop + cursorY;\n      pendingTargetRef.current = {\n        cursorX,\n        cursorY,\n        docX,\n        docY,\n        oldScale: scale,\n      };\n      setFitMode(null);\n      setScaleState((current) => {\n        const target = e.deltaY > 0 ? nextZoomOut(current) : nextZoomIn(current);\n        return target;\n      });\n    };\n\n    container.addEventListener(\"wheel\", onWheel, { passive: false });\n    return () => container.removeEventListener(\"wheel\", onWheel);\n  }, [containerRef, scale]);\n\n  // Pinch-zoom on touch devices via Pointer Events.\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const pointers = new Map<number, { x: number; y: number }>();\n    let initialDistance = 0;\n    let initialScale = scale;\n    let initialMid = { x: 0, y: 0 };\n    let initialDocPoint = { x: 0, y: 0 };\n\n    const distance = () => {\n      const pts = Array.from(pointers.values());\n      if (pts.length < 2) return 0;\n      const dx = pts[0].x - pts[1].x;\n      const dy = pts[0].y - pts[1].y;\n      return Math.hypot(dx, dy);\n    };\n\n    const midpoint = () => {\n      const pts = Array.from(pointers.values());\n      if (pts.length < 2) return { x: 0, y: 0 };\n      return { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };\n    };\n\n    const onPointerDown = (e: PointerEvent) => {\n      if (e.pointerType !== \"touch\") return;\n      pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n      if (pointers.size === 2) {\n        initialDistance = distance();\n        initialScale = scale;\n        const rect = container.getBoundingClientRect();\n        const mid = midpoint();\n        initialMid = {\n          x: mid.x - rect.left,\n          y: mid.y - rect.top,\n        };\n        initialDocPoint = {\n          x: container.scrollLeft + initialMid.x,\n          y: container.scrollTop + initialMid.y,\n        };\n      }\n    };\n\n    const onPointerMove = (e: PointerEvent) => {\n      if (e.pointerType !== \"touch\") return;\n      if (!pointers.has(e.pointerId)) return;\n      pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n      if (pointers.size === 2 && initialDistance > 0) {\n        e.preventDefault();\n        const newDistance = distance();\n        const ratio = newDistance / initialDistance;\n        const next = clampScale(initialScale * ratio);\n        pendingTargetRef.current = {\n          cursorX: initialMid.x,\n          cursorY: initialMid.y,\n          docX: initialDocPoint.x,\n          docY: initialDocPoint.y,\n          oldScale: initialScale,\n        };\n        setFitMode(null);\n        setScaleState(next);\n      }\n    };\n\n    const onPointerUp = (e: PointerEvent) => {\n      if (e.pointerType !== \"touch\") return;\n      pointers.delete(e.pointerId);\n      if (pointers.size < 2) {\n        initialDistance = 0;\n      }\n    };\n\n    container.addEventListener(\"pointerdown\", onPointerDown);\n    container.addEventListener(\"pointermove\", onPointerMove, { passive: false });\n    container.addEventListener(\"pointerup\", onPointerUp);\n    container.addEventListener(\"pointercancel\", onPointerUp);\n    return () => {\n      container.removeEventListener(\"pointerdown\", onPointerDown);\n      container.removeEventListener(\"pointermove\", onPointerMove);\n      container.removeEventListener(\"pointerup\", onPointerUp);\n      container.removeEventListener(\"pointercancel\", onPointerUp);\n    };\n  }, [containerRef, scale]);\n\n  return {\n    scale,\n    fitMode,\n    setScale,\n    zoomIn,\n    zoomOut,\n    resetZoom,\n  };\n}\n\nexport const PDF_VIEWER_SCALE_BOUNDS = {\n  min: PDF_VIEWER_MIN_SCALE,\n  max: PDF_VIEWER_MAX_SCALE,\n};\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-zoom.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-page-tracker.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect } from \"react\";\n\ninterface UsePdfPageTrackerArgs {\n  containerRef: { current: HTMLElement | null };\n  numPages: number;\n  ready: boolean;\n  onPageChange: (page: number) => void;\n}\n\nexport interface UsePdfPageTrackerReturn {\n  scrollToPage: (page: number, behavior?: ScrollBehavior) => void;\n}\n\n/**\n * Tracks which page is \"current\" based on which page element is closest to\n * the top of the scroll container. Uses IntersectionObserver with a tight\n * top-band root margin so the active page = the topmost intersecting page.\n */\nexport function usePdfPageTracker(\n  args: UsePdfPageTrackerArgs,\n): UsePdfPageTrackerReturn {\n  const { containerRef, numPages, ready, onPageChange } = args;\n\n  useEffect(() => {\n    if (!ready) return;\n    const container = containerRef.current;\n    if (!container) return;\n    if (numPages <= 0) return;\n\n    let frame: number | null = null;\n\n    const update = () => {\n      frame = null;\n      const pageEls = container.querySelectorAll<HTMLElement>(\n        \"[data-pdf-page]\",\n      );\n      if (pageEls.length === 0) return;\n      const containerTop = container.getBoundingClientRect().top;\n      const probeY = containerTop + 80;\n      let activePage = 1;\n      for (const el of Array.from(pageEls)) {\n        const rect = el.getBoundingClientRect();\n        if (rect.top <= probeY && rect.bottom > probeY) {\n          const n = Number(el.dataset.pdfPage);\n          if (Number.isFinite(n) && n > 0) {\n            activePage = n;\n            break;\n          }\n        }\n        if (rect.top > probeY) break;\n        const n = Number(el.dataset.pdfPage);\n        if (Number.isFinite(n) && n > 0) {\n          activePage = n;\n        }\n      }\n      onPageChange(activePage);\n    };\n\n    const onScroll = () => {\n      if (frame != null) return;\n      frame = requestAnimationFrame(update);\n    };\n\n    update();\n    container.addEventListener(\"scroll\", onScroll, { passive: true });\n    const ro = new ResizeObserver(onScroll);\n    ro.observe(container);\n    return () => {\n      container.removeEventListener(\"scroll\", onScroll);\n      ro.disconnect();\n      if (frame != null) cancelAnimationFrame(frame);\n    };\n  }, [containerRef, numPages, ready, onPageChange]);\n\n  const scrollToPage = useCallback(\n    (page: number, behavior: ScrollBehavior = \"smooth\") => {\n      const container = containerRef.current;\n      if (!container) return;\n      const target = container.querySelector<HTMLElement>(\n        `[data-pdf-page=\"${page}\"]`,\n      );\n      if (!target) return;\n      const containerRect = container.getBoundingClientRect();\n      const targetRect = target.getBoundingClientRect();\n      const top =\n        container.scrollTop + (targetRect.top - containerRect.top) - 8;\n      container.scrollTo({ top, behavior });\n    },\n    [containerRef],\n  );\n\n  return { scrollToPage };\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-page-tracker.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-virtualization.ts",
      "content": "\"use client\";\n\nimport type { PdfVirtualizeMode } from \"../types\";\n\ninterface ResolveArgs {\n  numPages: number;\n  currentPage: number;\n  mode: PdfVirtualizeMode;\n  threshold: number;\n  /** Pages to render before the current page. */\n  bufferAbove?: number;\n  /** Pages to render after the current page. */\n  bufferBelow?: number;\n}\n\n/**\n * Resolves which 1-based page numbers should be rendered as full <Page>\n * components. Other pages are rendered as height-locked placeholders.\n *\n * Rules:\n *   - 'never' OR ('auto' && numPages < threshold) → render all pages\n *   - 'always' OR ('auto' && numPages >= threshold) → window around current\n */\nexport function resolveVisiblePages({\n  numPages,\n  currentPage,\n  mode,\n  threshold,\n  bufferAbove = 1,\n  bufferBelow = 2,\n}: ResolveArgs): Set<number> {\n  if (numPages <= 0) return new Set();\n  const useVirtual =\n    mode === \"always\" || (mode === \"auto\" && numPages >= threshold);\n\n  if (!useVirtual) {\n    const all = new Set<number>();\n    for (let i = 1; i <= numPages; i++) all.add(i);\n    return all;\n  }\n\n  const start = Math.max(1, currentPage - bufferAbove);\n  const end = Math.min(numPages, currentPage + bufferBelow);\n  const visible = new Set<number>();\n  for (let i = start; i <= end; i++) visible.add(i);\n  return visible;\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-virtualization.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-keyboard.ts",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { PdfActions, PdfStatus } from \"../types\";\n\ninterface UsePdfKeyboardArgs {\n  containerRef: { current: HTMLElement | null };\n  rootRef: { current: HTMLElement | null };\n  actions: PdfActions;\n  status: PdfStatus;\n  allowDownload: boolean;\n  allowPrint: boolean;\n}\n\n/**\n * Wires keyboard shortcuts on the viewer root: PgUp/PgDn for page nav,\n * Cmd/Ctrl ± for zoom, Cmd/Ctrl + 0 for reset, Cmd/Ctrl + P for print,\n * Cmd/Ctrl + S for download. Listener attaches to document but only fires\n * when focus is inside the viewer's root.\n */\nexport function usePdfKeyboard(args: UsePdfKeyboardArgs): void {\n  const { rootRef, actions, status, allowDownload, allowPrint } = args;\n\n  useEffect(() => {\n    if (status !== \"ready\") return;\n\n    const onKeyDown = (e: KeyboardEvent) => {\n      const root = rootRef.current;\n      if (!root) return;\n      const target = e.target as Node | null;\n      if (target && !(root === target || root.contains(target))) return;\n\n      // Skip when typing in inputs / textareas / contenteditable.\n      if (\n        target instanceof HTMLElement &&\n        (target.tagName === \"INPUT\" ||\n          target.tagName === \"TEXTAREA\" ||\n          target.isContentEditable)\n      ) {\n        return;\n      }\n\n      const meta = e.ctrlKey || e.metaKey;\n\n      if (meta && (e.key === \"+\" || e.key === \"=\")) {\n        e.preventDefault();\n        actions.zoomIn();\n        return;\n      }\n      if (meta && e.key === \"-\") {\n        e.preventDefault();\n        actions.zoomOut();\n        return;\n      }\n      if (meta && e.key === \"0\") {\n        e.preventDefault();\n        actions.resetZoom();\n        return;\n      }\n      if (meta && (e.key === \"p\" || e.key === \"P\")) {\n        if (!allowPrint) return;\n        e.preventDefault();\n        actions.print();\n        return;\n      }\n      if (meta && (e.key === \"s\" || e.key === \"S\")) {\n        if (!allowDownload) return;\n        e.preventDefault();\n        actions.download();\n        return;\n      }\n      if (e.key === \"PageDown\") {\n        e.preventDefault();\n        actions.goToNextPage();\n        return;\n      }\n      if (e.key === \"PageUp\") {\n        e.preventDefault();\n        actions.goToPrevPage();\n        return;\n      }\n      if (e.key === \"Home\" && !meta) {\n        // bare Home — scroll to first page\n        if (target instanceof HTMLElement && target.tagName === \"INPUT\") return;\n        e.preventDefault();\n        actions.goToPage(1);\n        return;\n      }\n      if (e.key === \"End\" && !meta) {\n        if (target instanceof HTMLElement && target.tagName === \"INPUT\") return;\n        e.preventDefault();\n        actions.goToPage(Number.MAX_SAFE_INTEGER);\n        return;\n      }\n    };\n\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, [rootRef, actions, status, allowDownload, allowPrint]);\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-keyboard.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-drop.ts",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useMemo,\n  useRef,\n  useState,\n  type DragEvent as ReactDragEvent,\n  type HTMLAttributes,\n} from \"react\";\nimport { isPdfFile } from \"../lib/normalize-source\";\n\ninterface UsePdfDropArgs {\n  enabled: boolean;\n  onPdfDropped: (file: File) => void;\n  onNonPdfDropped?: () => void;\n}\n\nexport interface UsePdfDropReturn {\n  isDragging: boolean;\n  dropProps: Pick<\n    HTMLAttributes<HTMLElement>,\n    \"onDragEnter\" | \"onDragOver\" | \"onDragLeave\" | \"onDrop\"\n  >;\n}\n\n/**\n * Drag-drop a PDF onto the viewer. Tracks an `isDragging` flag with\n * counter-based dragenter/leave so nested children don't flicker the overlay.\n *\n * `isDragging` is gated by `enabled` at read time, so flipping `enabled` to\n * false while a drag is in progress hides the overlay without needing to\n * imperatively reset state in an effect.\n */\nexport function usePdfDrop(args: UsePdfDropArgs): UsePdfDropReturn {\n  const { enabled, onPdfDropped, onNonPdfDropped } = args;\n  const [isDraggingState, setIsDragging] = useState(false);\n  const counterRef = useRef(0);\n  const isDragging = enabled && isDraggingState;\n\n  const onDragEnter = useCallback(\n    (e: ReactDragEvent<HTMLElement>) => {\n      if (!enabled) return;\n      if (!e.dataTransfer.types.includes(\"Files\")) return;\n      counterRef.current += 1;\n      if (counterRef.current === 1) setIsDragging(true);\n    },\n    [enabled],\n  );\n\n  const onDragOver = useCallback(\n    (e: ReactDragEvent<HTMLElement>) => {\n      if (!enabled) return;\n      if (!e.dataTransfer.types.includes(\"Files\")) return;\n      e.preventDefault();\n      e.dataTransfer.dropEffect = \"copy\";\n    },\n    [enabled],\n  );\n\n  const onDragLeave = useCallback(\n    (e: ReactDragEvent<HTMLElement>) => {\n      if (!enabled) return;\n      if (!e.dataTransfer.types.includes(\"Files\")) return;\n      counterRef.current = Math.max(0, counterRef.current - 1);\n      if (counterRef.current === 0) setIsDragging(false);\n    },\n    [enabled],\n  );\n\n  const onDrop = useCallback(\n    (e: ReactDragEvent<HTMLElement>) => {\n      if (!enabled) return;\n      e.preventDefault();\n      counterRef.current = 0;\n      setIsDragging(false);\n      const files = Array.from(e.dataTransfer.files ?? []);\n      const pdf = files.find(isPdfFile);\n      if (pdf) {\n        onPdfDropped(pdf);\n      } else if (files.length > 0) {\n        onNonPdfDropped?.();\n      }\n    },\n    [enabled, onPdfDropped, onNonPdfDropped],\n  );\n\n  return useMemo(\n    () => ({\n      isDragging,\n      dropProps: { onDragEnter, onDragOver, onDragLeave, onDrop },\n    }),\n    [isDragging, onDragEnter, onDragOver, onDragLeave, onDrop],\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-drop.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-selection.ts",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\ninterface UsePdfSelectionArgs {\n  rootRef: { current: HTMLElement | null };\n  onSelection?: (args: { text: string }) => void;\n}\n\n/**\n * Tracks selected text inside the viewer. Debounced 150ms so we don't\n * fire on every mousemove during drag-select.\n */\nexport function usePdfSelection(args: UsePdfSelectionArgs): string {\n  const { rootRef, onSelection } = args;\n  const [selectedText, setSelectedText] = useState(\"\");\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n\n    let timeout: ReturnType<typeof setTimeout> | null = null;\n\n    const onSelectionChange = () => {\n      if (timeout != null) clearTimeout(timeout);\n      timeout = setTimeout(() => {\n        const sel = typeof window !== \"undefined\" ? window.getSelection() : null;\n        if (!sel || sel.rangeCount === 0) {\n          if (selectedText !== \"\") {\n            setSelectedText(\"\");\n            onSelection?.({ text: \"\" });\n          }\n          return;\n        }\n        const range = sel.getRangeAt(0);\n        const inViewer =\n          root === range.commonAncestorContainer ||\n          (range.commonAncestorContainer instanceof Node &&\n            root.contains(range.commonAncestorContainer));\n        if (!inViewer) return;\n        const text = sel.toString();\n        setSelectedText(text);\n        onSelection?.({ text });\n      }, 150);\n    };\n\n    document.addEventListener(\"selectionchange\", onSelectionChange);\n    return () => {\n      document.removeEventListener(\"selectionchange\", onSelectionChange);\n      if (timeout != null) clearTimeout(timeout);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [rootRef, onSelection]);\n\n  return selectedText;\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-selection.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/hooks/use-pdf-print.ts",
      "content": "\"use client\";\n\nimport { useCallback, useState } from \"react\";\nimport type { PDFDocumentProxy } from \"pdfjs-dist\";\nimport type { PdfRotation } from \"../types\";\n\ninterface UsePdfPrintArgs {\n  pdfDocument: PDFDocumentProxy | null;\n  rotation: PdfRotation;\n  /** Render scale for print. Higher = sharper output, more memory. Default: 2. */\n  printScale?: number;\n}\n\nexport interface UsePdfPrintReturn {\n  print: () => Promise<void>;\n  isPrinting: boolean;\n}\n\n/**\n * Print the loaded document by rendering each page to a high-DPI canvas,\n * embedding them in a hidden iframe as images, and triggering print() on\n * that iframe. Sharper output than printing the live viewer canvas.\n */\nexport function usePdfPrint(args: UsePdfPrintArgs): UsePdfPrintReturn {\n  const { pdfDocument, rotation, printScale = 2 } = args;\n  const [isPrinting, setIsPrinting] = useState(false);\n\n  const print = useCallback(async () => {\n    if (isPrinting) return;\n    if (!pdfDocument) return;\n    setIsPrinting(true);\n\n    const iframe = document.createElement(\"iframe\");\n    iframe.setAttribute(\"aria-hidden\", \"true\");\n    iframe.style.position = \"fixed\";\n    iframe.style.right = \"0\";\n    iframe.style.bottom = \"0\";\n    iframe.style.width = \"0\";\n    iframe.style.height = \"0\";\n    iframe.style.border = \"0\";\n    iframe.style.opacity = \"0\";\n    iframe.style.pointerEvents = \"none\";\n    document.body.appendChild(iframe);\n\n    const cleanup = () => {\n      try {\n        if (iframe.parentNode) iframe.parentNode.removeChild(iframe);\n      } catch {\n        // ignore\n      }\n      setIsPrinting(false);\n    };\n\n    try {\n      const doc = iframe.contentDocument;\n      const win = iframe.contentWindow;\n      if (!doc || !win) {\n        cleanup();\n        return;\n      }\n\n      doc.open();\n      doc.write(\n        `<!DOCTYPE html><html><head><style>` +\n          `@page { margin: 0; size: auto; }` +\n          `html, body { margin: 0; padding: 0; background: #fff; }` +\n          `.pdf-print-page { page-break-after: always; display: flex; align-items: center; justify-content: center; }` +\n          `.pdf-print-page:last-child { page-break-after: auto; }` +\n          `img { display: block; max-width: 100%; max-height: 100vh; }` +\n          `</style></head><body></body></html>`,\n      );\n      doc.close();\n\n      const body = doc.body;\n      const numPages = pdfDocument.numPages;\n\n      for (let i = 1; i <= numPages; i++) {\n        const page = await pdfDocument.getPage(i);\n        const viewport = page.getViewport({ scale: printScale, rotation });\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = Math.floor(viewport.width);\n        canvas.height = Math.floor(viewport.height);\n        const ctx = canvas.getContext(\"2d\");\n        if (!ctx) continue;\n        await page.render({\n          canvas,\n          canvasContext: ctx,\n          viewport,\n        } as Parameters<typeof page.render>[0]).promise;\n\n        const dataUrl = canvas.toDataURL(\"image/png\");\n        const wrapper = doc.createElement(\"div\");\n        wrapper.className = \"pdf-print-page\";\n        const img = doc.createElement(\"img\");\n        img.src = dataUrl;\n        wrapper.appendChild(img);\n        body.appendChild(wrapper);\n\n        canvas.width = 0;\n        canvas.height = 0;\n      }\n\n      const onAfterPrint = () => {\n        win.removeEventListener(\"afterprint\", onAfterPrint);\n        cleanup();\n      };\n      win.addEventListener(\"afterprint\", onAfterPrint);\n\n      win.focus();\n      win.print();\n\n      // Safety net in case afterprint never fires.\n      setTimeout(cleanup, 60000);\n    } catch (err) {\n      cleanup();\n      throw err;\n    }\n  }, [pdfDocument, rotation, printScale, isPrinting]);\n\n  return { print, isPrinting };\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/hooks/use-pdf-print.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/lib/clamp-scale.ts",
      "content": "export const PDF_VIEWER_MIN_SCALE = 0.25;\nexport const PDF_VIEWER_MAX_SCALE = 5;\nexport const PDF_VIEWER_ZOOM_STEP = 1.2;\n\nexport function clampScale(scale: number): number {\n  if (Number.isNaN(scale) || !Number.isFinite(scale)) return 1;\n  return Math.max(PDF_VIEWER_MIN_SCALE, Math.min(PDF_VIEWER_MAX_SCALE, scale));\n}\n\nexport function nextZoomIn(scale: number): number {\n  return clampScale(scale * PDF_VIEWER_ZOOM_STEP);\n}\n\nexport function nextZoomOut(scale: number): number {\n  return clampScale(scale / PDF_VIEWER_ZOOM_STEP);\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/lib/clamp-scale.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/lib/compute-fit-scale.ts",
      "content": "import { clampScale } from \"./clamp-scale\";\n\nexport interface FitInputs {\n  containerWidth: number;\n  containerHeight: number;\n  pageWidth: number;\n  pageHeight: number;\n  rotation?: 0 | 90 | 180 | 270;\n  /** Horizontal padding to subtract from container width before fitting. */\n  horizontalPadding?: number;\n  /** Vertical padding to subtract from container height before fitting (toolbar etc). */\n  verticalPadding?: number;\n}\n\nfunction rotatedSize(\n  width: number,\n  height: number,\n  rotation: number,\n): [number, number] {\n  return rotation % 180 === 0 ? [width, height] : [height, width];\n}\n\nexport function computeFitWidthScale(inputs: FitInputs): number {\n  const rotation = inputs.rotation ?? 0;\n  const [pw] = rotatedSize(inputs.pageWidth, inputs.pageHeight, rotation);\n  const horizontalPadding = inputs.horizontalPadding ?? 32;\n  const usable = Math.max(0, inputs.containerWidth - horizontalPadding);\n  if (pw <= 0 || usable <= 0) return 1;\n  return clampScale(usable / pw);\n}\n\nexport function computeFitPageScale(inputs: FitInputs): number {\n  const rotation = inputs.rotation ?? 0;\n  const [pw, ph] = rotatedSize(\n    inputs.pageWidth,\n    inputs.pageHeight,\n    rotation,\n  );\n  const horizontalPadding = inputs.horizontalPadding ?? 32;\n  const verticalPadding = inputs.verticalPadding ?? 32;\n  const usableW = Math.max(0, inputs.containerWidth - horizontalPadding);\n  const usableH = Math.max(0, inputs.containerHeight - verticalPadding);\n  if (pw <= 0 || ph <= 0 || usableW <= 0 || usableH <= 0) return 1;\n  return clampScale(Math.min(usableW / pw, usableH / ph));\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/lib/compute-fit-scale.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/lib/normalize-source.ts",
      "content": "import type { PdfSource } from \"../types\";\n\n/**\n * react-pdf accepts:\n *   - string (URL or data URI)\n *   - { data: Uint8Array | ArrayBuffer }\n *   - Blob (covers File, since File extends Blob)\n *\n * We unify all of those into something the <Document file={...}> prop accepts.\n */\nexport type NormalizedPdfSource =\n  | string\n  | { data: Uint8Array | ArrayBuffer }\n  | Blob;\n\nexport function normalizeSource(\n  input: PdfSource | null | undefined,\n): NormalizedPdfSource | null {\n  if (input == null) return null;\n  if (typeof input === \"string\") return input;\n  if (input instanceof ArrayBuffer) return { data: input };\n  if (input instanceof Uint8Array) return { data: input };\n  if (input instanceof Blob) return input;\n  return null;\n}\n\nexport function isPdfFile(file: File): boolean {\n  return (\n    file.type === \"application/pdf\" ||\n    file.name.toLowerCase().endsWith(\".pdf\")\n  );\n}\n\nexport function getSourceFilename(\n  source: PdfSource | null | undefined,\n): string | null {\n  if (source == null) return null;\n  if (typeof source === \"string\") {\n    try {\n      const url = new URL(source, \"http://placeholder.local\");\n      const name = url.pathname.split(\"/\").pop();\n      return name && name.length > 0 ? decodeURIComponent(name) : null;\n    } catch {\n      return null;\n    }\n  }\n  if (typeof File !== \"undefined\" && source instanceof File) {\n    return source.name || null;\n  }\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/lib/normalize-source.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/lib/download.ts",
      "content": "import type { PdfSource } from \"../types\";\n\nfunction clickAnchor(href: string, filename: string): void {\n  const a = document.createElement(\"a\");\n  a.href = href;\n  a.download = filename;\n  a.rel = \"noopener\";\n  a.style.display = \"none\";\n  document.body.appendChild(a);\n  a.click();\n  document.body.removeChild(a);\n}\n\nasync function blobFromUrl(url: string): Promise<Blob> {\n  const res = await fetch(url, { credentials: \"same-origin\" });\n  if (!res.ok) {\n    throw new Error(`Failed to fetch PDF for download (${res.status})`);\n  }\n  return await res.blob();\n}\n\nexport async function downloadAsFile(\n  source: PdfSource | null,\n  filename: string,\n): Promise<void> {\n  if (source == null) return;\n  const finalName = filename.toLowerCase().endsWith(\".pdf\")\n    ? filename\n    : `${filename}.pdf`;\n\n  if (typeof source === \"string\") {\n    let isCrossOrigin = false;\n    try {\n      const url = new URL(source, window.location.href);\n      isCrossOrigin = url.origin !== window.location.origin;\n    } catch {\n      isCrossOrigin = false;\n    }\n    if (!isCrossOrigin) {\n      clickAnchor(source, finalName);\n      return;\n    }\n    const blob = await blobFromUrl(source);\n    const objectUrl = URL.createObjectURL(blob);\n    try {\n      clickAnchor(objectUrl, finalName);\n    } finally {\n      setTimeout(() => URL.revokeObjectURL(objectUrl), 5000);\n    }\n    return;\n  }\n\n  let blob: Blob;\n  if (source instanceof Blob) {\n    blob = source;\n  } else if (source instanceof ArrayBuffer) {\n    blob = new Blob([source], { type: \"application/pdf\" });\n  } else if (source instanceof Uint8Array) {\n    blob = new Blob([new Uint8Array(source)], { type: \"application/pdf\" });\n  } else {\n    return;\n  }\n\n  const objectUrl = URL.createObjectURL(blob);\n  try {\n    clickAnchor(objectUrl, finalName);\n  } finally {\n    setTimeout(() => URL.revokeObjectURL(objectUrl), 5000);\n  }\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/lib/download.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/lib/worker-config.ts",
      "content": "import { pdfjs } from \"react-pdf\";\n\nlet appliedSrc: string | null = null;\n\n/**\n * pdf.js's Web Worker. Default: load from the unpkg CDN at the same version\n * react-pdf bundles (`pdfjs.version` is set by react-pdf at module init).\n *\n * For offline-first apps, copy `pdfjs-dist/build/pdf.worker.min.mjs` into\n * your `public/` directory at build time and pass that path via the\n * `workerSrc` prop, e.g. `<PdfViewer workerSrc=\"/pdf.worker.min.mjs\" />`.\n *\n * The CDN default keeps the component drop-in without bundler-specific asset\n * imports (which vary across Webpack 5 / Turbopack / Vite / older bundlers).\n */\nfunction defaultWorkerSrc(): string {\n  return `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;\n}\n\nexport function ensureWorkerConfigured(override?: string): void {\n  const desired = override ?? defaultWorkerSrc();\n  if (appliedSrc !== desired) {\n    pdfjs.GlobalWorkerOptions.workerSrc = desired;\n    appliedSrc = desired;\n  }\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/lib/worker-config.ts"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-page.tsx",
      "content": "\"use client\";\n\nimport { Page } from \"react-pdf\";\nimport type { PDFDocumentProxy } from \"pdfjs-dist\";\nimport { cn } from \"@/lib/utils\";\nimport type { PdfRotation } from \"../types\";\n\ninterface PdfPageProps {\n  pdfDocument: PDFDocumentProxy;\n  pageNumber: number;\n  scale: number;\n  rotation: PdfRotation;\n  /** Known intrinsic page size at scale=1, used for the placeholder when not visible. */\n  basePageSize: { width: number; height: number } | null;\n  /** When true, render only a placeholder div with the right height (virtualized away). */\n  placeholder: boolean;\n  className?: string;\n}\n\nexport function PdfPage({\n  pdfDocument,\n  pageNumber,\n  scale,\n  rotation,\n  basePageSize,\n  placeholder,\n  className,\n}: PdfPageProps) {\n  if (placeholder) {\n    const rotated = rotation % 180 === 0;\n    const w = basePageSize\n      ? (rotated ? basePageSize.width : basePageSize.height) * scale\n      : 600;\n    const h = basePageSize\n      ? (rotated ? basePageSize.height : basePageSize.width) * scale\n      : 800;\n    return (\n      <div\n        data-pdf-page={pageNumber}\n        data-pdf-page-placeholder\n        className={cn(\n          \"relative mx-auto rounded-md border border-dashed border-border bg-muted/30\",\n          className,\n        )}\n        style={{ width: w, height: h }}\n        aria-hidden=\"true\"\n      />\n    );\n  }\n\n  return (\n    <div\n      data-pdf-page={pageNumber}\n      className={cn(\"relative mx-auto\", className)}\n    >\n      <Page\n        pdf={pdfDocument}\n        pageNumber={pageNumber}\n        scale={scale}\n        rotate={rotation}\n        renderTextLayer={true}\n        renderAnnotationLayer={true}\n        loading={\n          <div\n            className=\"rounded-md border border-border bg-muted/30\"\n            style={{\n              width: basePageSize ? basePageSize.width * scale : 600,\n              height: basePageSize ? basePageSize.height * scale : 800,\n            }}\n          />\n        }\n        className=\"overflow-hidden rounded-md shadow-sm ring-1 ring-border\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-page.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-toolbar.tsx",
      "content": "\"use client\";\n\nimport { Separator } from \"@/components/ui/separator\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { usePdfViewer } from \"../hooks/use-pdf-viewer-context\";\nimport { PdfActionMenu } from \"./pdf-action-menu\";\nimport { PdfPageIndicator } from \"./pdf-page-indicator\";\nimport { PdfPageNav } from \"./pdf-page-nav\";\nimport { PdfZoomControls } from \"./pdf-zoom-controls\";\n\ninterface PdfToolbarProps {\n  className?: string;\n}\n\nexport function PdfToolbar({ className }: PdfToolbarProps) {\n  const { labels } = usePdfViewer();\n\n  return (\n    <TooltipProvider>\n      <div\n        role=\"toolbar\"\n        aria-label={labels.toolbarAriaLabel}\n        className={cn(\n          \"flex items-center gap-1 border-b border-border bg-card px-2 py-1.5\",\n          className,\n        )}\n      >\n        <PdfPageNav />\n        <PdfPageIndicator />\n        <Separator orientation=\"vertical\" className=\"mx-1 h-5\" />\n        <PdfZoomControls />\n        <Separator orientation=\"vertical\" className=\"mx-1 h-5\" />\n        <div className=\"ml-auto flex items-center gap-0.5\">\n          <PdfActionMenu />\n        </div>\n      </div>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-toolbar.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-page-nav.tsx",
      "content": "\"use client\";\n\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { usePdfViewer } from \"../hooks/use-pdf-viewer-context\";\n\ninterface PdfPageNavProps {\n  className?: string;\n}\n\nexport function PdfPageNav({ className }: PdfPageNavProps) {\n  const { page, numPages, actions, labels, status } = usePdfViewer();\n  const disabled = status !== \"ready\" || numPages === 0;\n\n  // F-cross-13: no `asChild` — triggers render their own <button> in both\n  // backends; Button look inlined via buttonVariants.\n  return (\n    <div className={cn(\"flex items-center gap-0.5\", className)}>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n          onClick={() => actions.goToPrevPage()}\n          disabled={disabled || page <= 1}\n          aria-label={typeof labels.prevPage === \"string\" ? labels.prevPage : \"Previous page\"}\n        >\n          <ChevronLeft />\n        </TooltipTrigger>\n        <TooltipContent>{labels.prevPage}</TooltipContent>\n      </Tooltip>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n          onClick={() => actions.goToNextPage()}\n          disabled={disabled || page >= numPages}\n          aria-label={typeof labels.nextPage === \"string\" ? labels.nextPage : \"Next page\"}\n        >\n          <ChevronRight />\n        </TooltipTrigger>\n        <TooltipContent>{labels.nextPage}</TooltipContent>\n      </Tooltip>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-page-nav.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-page-indicator.tsx",
      "content": "\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { usePdfViewer } from \"../hooks/use-pdf-viewer-context\";\n\ninterface PdfPageIndicatorProps {\n  className?: string;\n}\n\nexport function PdfPageIndicator({ className }: PdfPageIndicatorProps) {\n  const { page, numPages, actions, labels, status } = usePdfViewer();\n  // Override is set while the user is editing; cleared on commit/blur. The\n  // displayed value falls through to the live `page` when no override exists.\n  const [override, setOverride] = useState<string | null>(null);\n  const draft = override ?? String(page);\n\n  const onSubmit = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    const parsed = Number.parseInt(draft, 10);\n    if (Number.isFinite(parsed) && parsed >= 1) {\n      actions.goToPage(Math.min(parsed, numPages || parsed));\n    }\n    setOverride(null);\n    (e.currentTarget.querySelector(\"input\") as HTMLInputElement | null)?.blur();\n  };\n\n  const disabled = status !== \"ready\" || numPages === 0;\n\n  return (\n    <form\n      onSubmit={onSubmit}\n      className={cn(\n        \"flex items-center gap-1.5 font-mono text-xs tabular-nums text-foreground\",\n        className,\n      )}\n    >\n      <Input\n        aria-label={\n          typeof labels.jumpToPage === \"string\" ? labels.jumpToPage : \"Jump to page\"\n        }\n        type=\"text\"\n        inputMode=\"numeric\"\n        pattern=\"[0-9]*\"\n        value={draft}\n        onChange={(e) => {\n          setOverride(e.target.value.replace(/[^0-9]/g, \"\"));\n        }}\n        onFocus={(e) => {\n          setOverride(String(page));\n          e.currentTarget.select();\n        }}\n        onBlur={() => {\n          setOverride(null);\n        }}\n        disabled={disabled}\n        className=\"h-7 w-12 px-1.5 text-center font-mono text-xs tabular-nums\"\n      />\n      <span className=\"text-muted-foreground\">/</span>\n      <span className=\"min-w-6 text-left text-muted-foreground\">\n        {numPages || \"—\"}\n      </span>\n    </form>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-page-indicator.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-zoom-controls.tsx",
      "content": "\"use client\";\n\nimport { ChevronDown, Minus, Plus } from \"lucide-react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  PDF_VIEWER_MAX_SCALE,\n  PDF_VIEWER_MIN_SCALE,\n} from \"../lib/clamp-scale\";\nimport { usePdfViewer } from \"../hooks/use-pdf-viewer-context\";\n\nconst PRESETS: number[] = [0.5, 0.75, 1, 1.25, 1.5, 2];\n\ninterface PdfZoomControlsProps {\n  className?: string;\n}\n\nexport function PdfZoomControls({ className }: PdfZoomControlsProps) {\n  const { scale, fitMode, actions, labels, status } = usePdfViewer();\n  const disabled = status !== \"ready\";\n\n  // F-cross-13: no `asChild` — triggers render their own <button> in both\n  // backends; Button look inlined via buttonVariants (Base-UI primitives\n  // lack Slot support).\n  return (\n    <div className={cn(\"flex items-center gap-0.5\", className)}>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n          onClick={() => actions.zoomOut()}\n          disabled={disabled || scale <= PDF_VIEWER_MIN_SCALE + 1e-3}\n          aria-label={typeof labels.zoomOut === \"string\" ? labels.zoomOut : \"Zoom out\"}\n        >\n          <Minus />\n        </TooltipTrigger>\n        <TooltipContent>{labels.zoomOut}</TooltipContent>\n      </Tooltip>\n\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          type=\"button\"\n          disabled={disabled}\n          className={cn(\n            buttonVariants({ variant: \"ghost\", size: \"sm\" }),\n            \"min-w-[4.5rem] gap-1 font-mono tabular-nums\",\n          )}\n        >\n          {fitMode === \"fit-width\"\n            ? labels.fitWidth\n            : fitMode === \"fit-page\"\n              ? labels.fitPage\n              : labels.scalePercent(scale)}\n          <ChevronDown className=\"size-3 opacity-60\" aria-hidden=\"true\" />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"center\" className=\"min-w-36\">\n          <DropdownMenuItem onSelect={() => actions.setScale(\"fit-width\")}>\n            {labels.fitWidth}\n          </DropdownMenuItem>\n          <DropdownMenuItem onSelect={() => actions.setScale(\"fit-page\")}>\n            {labels.fitPage}\n          </DropdownMenuItem>\n          <DropdownMenuSeparator />\n          {PRESETS.map((preset) => (\n            <DropdownMenuItem\n              key={preset}\n              onSelect={() => actions.setScale(preset)}\n              className=\"font-mono tabular-nums\"\n            >\n              {labels.scalePercent(preset)}\n            </DropdownMenuItem>\n          ))}\n          <DropdownMenuSeparator />\n          <DropdownMenuItem onSelect={() => actions.resetZoom()}>\n            {labels.resetZoom}\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n          onClick={() => actions.zoomIn()}\n          disabled={disabled || scale >= PDF_VIEWER_MAX_SCALE - 1e-3}\n          aria-label={typeof labels.zoomIn === \"string\" ? labels.zoomIn : \"Zoom in\"}\n        >\n          <Plus />\n        </TooltipTrigger>\n        <TooltipContent>{labels.zoomIn}</TooltipContent>\n      </Tooltip>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-zoom-controls.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-action-menu.tsx",
      "content": "\"use client\";\n\nimport { Download, MoreVertical, Printer, RotateCw } from \"lucide-react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { usePdfViewer } from \"../hooks/use-pdf-viewer-context\";\n\ninterface PdfActionMenuProps {\n  /** Force compact mode (overflow). Default: respects viewer's `compact` flag. */\n  compact?: boolean;\n  className?: string;\n}\n\nexport function PdfActionMenu({ compact, className }: PdfActionMenuProps) {\n  const ctx = usePdfViewer();\n  const { actions, labels, status, allowDownload, allowPrint } = ctx;\n  const useCompact = compact ?? ctx.compact;\n  const disabled = status !== \"ready\";\n\n  // F-cross-13: no `asChild` — triggers render their own <button> in both\n  // backends; Button look inlined via buttonVariants.\n  if (useCompact) {\n    return (\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          type=\"button\"\n          disabled={disabled}\n          aria-label={typeof labels.more === \"string\" ? labels.more : \"More actions\"}\n          className={cn(\n            buttonVariants({ variant: \"ghost\", size: \"icon-sm\" }),\n            className,\n          )}\n        >\n          <MoreVertical />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\">\n          <DropdownMenuItem onSelect={() => actions.rotate()}>\n            <RotateCw aria-hidden=\"true\" />\n            {labels.rotate}\n          </DropdownMenuItem>\n          {allowDownload ? (\n            <DropdownMenuItem onSelect={() => actions.download()}>\n              <Download aria-hidden=\"true\" />\n              {labels.download}\n            </DropdownMenuItem>\n          ) : null}\n          {allowPrint ? (\n            <DropdownMenuItem onSelect={() => actions.print()}>\n              <Printer aria-hidden=\"true\" />\n              {labels.print}\n            </DropdownMenuItem>\n          ) : null}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    );\n  }\n\n  return (\n    <div className={cn(\"flex items-center gap-0.5\", className)}>\n      <Tooltip>\n        <TooltipTrigger\n          type=\"button\"\n          className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n          onClick={() => actions.rotate()}\n          disabled={disabled}\n          aria-label={typeof labels.rotate === \"string\" ? labels.rotate : \"Rotate\"}\n        >\n          <RotateCw />\n        </TooltipTrigger>\n        <TooltipContent>{labels.rotate}</TooltipContent>\n      </Tooltip>\n      {allowDownload ? (\n        <Tooltip>\n          <TooltipTrigger\n            type=\"button\"\n            className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n            onClick={() => actions.download()}\n            disabled={disabled}\n            aria-label={typeof labels.download === \"string\" ? labels.download : \"Download\"}\n          >\n            <Download />\n          </TooltipTrigger>\n          <TooltipContent>{labels.download}</TooltipContent>\n        </Tooltip>\n      ) : null}\n      {allowPrint ? (\n        <Tooltip>\n          <TooltipTrigger\n            type=\"button\"\n            className={buttonVariants({ variant: \"ghost\", size: \"icon-sm\" })}\n            onClick={() => actions.print()}\n            disabled={disabled}\n            aria-label={typeof labels.print === \"string\" ? labels.print : \"Print\"}\n          >\n            <Printer />\n          </TooltipTrigger>\n          <TooltipContent>{labels.print}</TooltipContent>\n        </Tooltip>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-action-menu.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-context-menu.tsx",
      "content": "\"use client\";\n\nimport {\n  ChevronLeft,\n  ChevronRight,\n  Copy,\n  Download,\n  Minus,\n  Plus,\n  Printer,\n  RotateCw,\n  Search,\n} from \"lucide-react\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuTrigger,\n} from \"@/components/ui/context-menu\";\nimport { usePdfViewer } from \"../hooks/use-pdf-viewer-context\";\nimport type { ReactNode } from \"react\";\n\ninterface PdfContextMenuProps {\n  enabled: boolean;\n  onSearchSelection?: (text: string) => void;\n  className?: string;\n  children: ReactNode;\n}\n\nasync function copyText(text: string): Promise<void> {\n  if (!text) return;\n  try {\n    await navigator.clipboard.writeText(text);\n  } catch {\n    // ignore — clipboard may be denied\n  }\n}\n\nexport function PdfContextMenu({\n  enabled,\n  onSearchSelection,\n  className,\n  children,\n}: PdfContextMenuProps) {\n  const { actions, labels, allowDownload, allowPrint, selectedText } =\n    usePdfViewer();\n\n  if (!enabled) {\n    return <div className={className}>{children}</div>;\n  }\n\n  const hasSelection = selectedText.trim().length > 0;\n  const showSearch = hasSelection && typeof onSearchSelection === \"function\";\n\n  return (\n    <ContextMenu>\n      {/* F-cross-13: no `asChild` — trigger is a display:contents wrapper;\n          right-click bubbles up from children in both backends. */}\n      <ContextMenuTrigger className=\"contents\">\n        <div className={className}>{children}</div>\n      </ContextMenuTrigger>\n      <ContextMenuContent className=\"min-w-44\">\n        {hasSelection ? (\n          <>\n            <ContextMenuItem onSelect={() => copyText(selectedText)}>\n              <Copy aria-hidden=\"true\" />\n              {labels.copyText}\n            </ContextMenuItem>\n            {showSearch ? (\n              <ContextMenuItem\n                onSelect={() => onSearchSelection?.(selectedText)}\n              >\n                <Search aria-hidden=\"true\" />\n                {labels.searchSelection}\n              </ContextMenuItem>\n            ) : null}\n            <ContextMenuSeparator />\n          </>\n        ) : null}\n        <ContextMenuItem onSelect={() => actions.goToPrevPage()}>\n          <ChevronLeft aria-hidden=\"true\" />\n          {labels.contextPrevPage}\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={() => actions.goToNextPage()}>\n          <ChevronRight aria-hidden=\"true\" />\n          {labels.contextNextPage}\n        </ContextMenuItem>\n        <ContextMenuSeparator />\n        <ContextMenuItem onSelect={() => actions.zoomIn()}>\n          <Plus aria-hidden=\"true\" />\n          {labels.contextZoomIn}\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={() => actions.zoomOut()}>\n          <Minus aria-hidden=\"true\" />\n          {labels.contextZoomOut}\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={() => actions.rotate()}>\n          <RotateCw aria-hidden=\"true\" />\n          {labels.contextRotate}\n        </ContextMenuItem>\n        {allowDownload || allowPrint ? <ContextMenuSeparator /> : null}\n        {allowDownload ? (\n          <ContextMenuItem onSelect={() => actions.download()}>\n            <Download aria-hidden=\"true\" />\n            {labels.contextDownload}\n          </ContextMenuItem>\n        ) : null}\n        {allowPrint ? (\n          <ContextMenuItem onSelect={() => actions.print()}>\n            <Printer aria-hidden=\"true\" />\n            {labels.contextPrint}\n          </ContextMenuItem>\n        ) : null}\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-context-menu.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-password-prompt.tsx",
      "content": "\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport { Lock } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPdfViewerLabels } from \"../types\";\n\ninterface PdfPasswordPromptProps {\n  open: boolean;\n  labels: ResolvedPdfViewerLabels;\n  error: Error | null;\n  attempts: number;\n  onSubmit: (password: string) => void;\n  onCancel: () => void;\n  className?: string;\n}\n\nexport function PdfPasswordPrompt({\n  open,\n  labels,\n  error,\n  attempts,\n  onSubmit,\n  onCancel,\n}: PdfPasswordPromptProps) {\n  const [value, setValue] = useState(\"\");\n  const showError = attempts > 0 && error;\n\n  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    if (!value) return;\n    onSubmit(value);\n    setValue(\"\");\n  };\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(next) => {\n        if (!next) onCancel();\n      }}\n    >\n      <DialogContent className=\"sm:max-w-md\">\n        <DialogHeader>\n          <div className=\"mb-1 flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n            <Lock className=\"size-5\" aria-hidden=\"true\" />\n          </div>\n          <DialogTitle>{labels.passwordTitle}</DialogTitle>\n          <DialogDescription>{labels.passwordHint}</DialogDescription>\n        </DialogHeader>\n        <form onSubmit={handleSubmit} className=\"space-y-3\">\n          <Input\n            autoFocus\n            type=\"password\"\n            value={value}\n            onChange={(e) => setValue(e.target.value)}\n            placeholder={labels.passwordPlaceholder}\n            aria-invalid={!!showError}\n            aria-describedby={showError ? \"pdf-viewer-password-error\" : undefined}\n          />\n          {showError ? (\n            <p\n              id=\"pdf-viewer-password-error\"\n              className={cn(\"text-xs text-destructive\")}\n              role=\"alert\"\n            >\n              {labels.passwordError}\n            </p>\n          ) : null}\n          <DialogFooter>\n            <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n              {labels.passwordCancel}\n            </Button>\n            <Button type=\"submit\" size=\"sm\" disabled={!value}>\n              {labels.passwordSubmit}\n            </Button>\n          </DialogFooter>\n        </form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-password-prompt.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-drop-overlay.tsx",
      "content": "\"use client\";\n\nimport { Upload } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPdfViewerLabels } from \"../types\";\n\ninterface PdfDropOverlayProps {\n  labels: ResolvedPdfViewerLabels;\n  className?: string;\n}\n\nexport function PdfDropOverlay({ labels, className }: PdfDropOverlayProps) {\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 z-30 flex items-center justify-center\",\n        \"rounded-[inherit] border-2 border-dashed border-primary\",\n        \"bg-primary/10 backdrop-blur-[2px]\",\n        className,\n      )}\n    >\n      <div className=\"flex flex-col items-center gap-2 rounded-lg bg-background/90 px-6 py-4 shadow-lg ring-1 ring-border\">\n        <Upload className=\"size-6 text-primary\" aria-hidden=\"true\" />\n        <span className=\"text-sm font-medium text-foreground\">\n          {labels.dropPdfHere}\n        </span>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-drop-overlay.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-empty-state.tsx",
      "content": "\"use client\";\n\nimport { FileText } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPdfViewerLabels } from \"../types\";\n\ninterface PdfEmptyStateProps {\n  labels: ResolvedPdfViewerLabels;\n  enableDragDrop: boolean;\n  className?: string;\n}\n\nexport function PdfEmptyState({\n  labels,\n  enableDragDrop,\n  className,\n}: PdfEmptyStateProps) {\n  return (\n    <div\n      className={cn(\n        \"flex h-full w-full flex-col items-center justify-center gap-3 p-12 text-center\",\n        className,\n      )}\n    >\n      <div className=\"flex size-14 items-center justify-center rounded-full bg-muted text-muted-foreground\">\n        <FileText className=\"size-7\" aria-hidden=\"true\" />\n      </div>\n      <div className=\"space-y-1\">\n        <p className=\"text-base font-semibold text-foreground\">\n          {labels.emptyTitle}\n        </p>\n        {enableDragDrop ? (\n          <p className=\"text-sm text-muted-foreground\">{labels.emptyHint}</p>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-empty-state.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-error-state.tsx",
      "content": "\"use client\";\n\nimport { AlertCircle, RefreshCcw } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPdfViewerLabels } from \"../types\";\n\ninterface PdfErrorStateProps {\n  error: Error;\n  labels: ResolvedPdfViewerLabels;\n  onRetry?: () => void;\n  className?: string;\n}\n\nexport function PdfErrorState({\n  error,\n  labels,\n  onRetry,\n  className,\n}: PdfErrorStateProps) {\n  return (\n    <div\n      role=\"alert\"\n      className={cn(\n        \"flex h-full w-full flex-col items-center justify-center gap-4 p-8 text-center\",\n        className,\n      )}\n    >\n      <div className=\"flex size-12 items-center justify-center rounded-full bg-destructive/10 text-destructive\">\n        <AlertCircle className=\"size-6\" aria-hidden=\"true\" />\n      </div>\n      <div className=\"space-y-1\">\n        <p className=\"text-base font-semibold text-foreground\">\n          {labels.errorTitle}\n        </p>\n        <p className=\"max-w-md text-sm text-muted-foreground\">{error.message}</p>\n      </div>\n      {onRetry ? (\n        <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n          <RefreshCcw className=\"size-3.5\" aria-hidden=\"true\" />\n          {labels.errorRetry}\n        </Button>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-error-state.tsx"
    },
    {
      "path": "src/registry/components/media/pdf-viewer/parts/pdf-loading-state.tsx",
      "content": "\"use client\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\nimport type { ResolvedPdfViewerLabels } from \"../types\";\n\ninterface PdfLoadingStateProps {\n  labels: ResolvedPdfViewerLabels;\n  className?: string;\n}\n\nexport function PdfLoadingState({ labels, className }: PdfLoadingStateProps) {\n  return (\n    <div\n      className={cn(\n        \"flex h-full w-full flex-col items-center justify-start gap-4 overflow-hidden bg-muted/20 p-8\",\n        className,\n      )}\n      aria-busy=\"true\"\n      aria-live=\"polite\"\n    >\n      <span className=\"sr-only\">{labels.loading}</span>\n      <Skeleton className=\"h-3 w-32\" />\n      <Skeleton className=\"h-[80vh] w-full max-w-2xl\" />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pdf-viewer/parts/pdf-loading-state.tsx"
    }
  ],
  "categories": [
    "media",
    "pdf",
    "viewer"
  ],
  "type": "registry:block"
}