{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdf-pages",
  "title": "PDF Pages",
  "description": "Renders every page of a PDF to canvases with pdf.js, fit-to-width with zoom and device-pixel-ratio aware; lazy-loads the pdf.js module and worker.",
  "dependencies": [
    "pdfjs-dist"
  ],
  "files": [
    {
      "path": "registry/sav/components/pdf-pages.tsx",
      "content": "// @sav-registry: pdf-pages\n\"use client\";\n\n// Renders every page of a PDF inside the document viewer, stacked and\n// scrollable, with pdf.js drawing each page to a canvas. Replaces the native\n// <iframe> viewer: Safari on iPhone shows an iframed PDF as its first page\n// only, and Android Chrome downloads it instead. Pages fit the frame width at\n// 100% and re-render crisply at each zoom step and device pixel ratio. The\n// pdf.js module and its worker load on first use only.\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { PDFDocumentLoadingTask, PDFDocumentProxy, RenderTask } from \"pdfjs-dist\";\n\ntype PdfJs = typeof import(\"pdfjs-dist\");\n\nlet pdfjsPromise: Promise<PdfJs> | null = null;\n\nfunction loadPdfjs(): Promise<PdfJs> {\n  if (!pdfjsPromise) {\n    pdfjsPromise = import(\"pdfjs-dist\").then((pdfjs) => {\n      // Bundled as a static asset next to the app (Turbopack `new URL` asset).\n      pdfjs.GlobalWorkerOptions.workerSrc = new URL(\n        \"pdfjs-dist/build/pdf.worker.min.mjs\",\n        import.meta.url,\n      ).toString();\n      return pdfjs;\n    });\n  }\n  return pdfjsPromise;\n}\n\n/** Horizontal padding around the pages, in CSS pixels, each side. */\nconst GUTTER = 16;\n\nexport function PdfPages({\n  url,\n  zoom,\n  label,\n  onError,\n}: {\n  url: string;\n  /** 1 = fit the frame width. */\n  zoom: number;\n  label: string;\n  /** Called once when the document cannot be loaded (the caller falls back). */\n  onError?: (message: string) => void;\n}) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [doc, setDoc] = useState<PDFDocumentProxy | null>(null);\n  const [frameWidth, setFrameWidth] = useState(0);\n  const [status, setStatus] = useState<\"loading\" | \"ready\" | \"error\">(\"loading\");\n\n  // Load the document; the loading task is destroyed (worker + fetch freed)\n  // when the url changes or the viewer closes.\n  useEffect(() => {\n    let cancelled = false;\n    let task: PDFDocumentLoadingTask | null = null;\n    loadPdfjs()\n      .then(async (pdfjs) => {\n        if (cancelled) return;\n        task = pdfjs.getDocument({ url });\n        const loaded = await task.promise;\n        if (cancelled) return;\n        setDoc(loaded);\n        setStatus(\"ready\");\n      })\n      .catch((error: unknown) => {\n        if (cancelled) return;\n        setStatus(\"error\");\n        onError?.(error instanceof Error ? error.message : String(error));\n      });\n    return () => {\n      cancelled = true;\n      task?.destroy().catch(() => undefined);\n    };\n    // onError is informational; re-running the load on its identity would\n    // re-fetch the PDF for nothing.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [url]);\n\n  // Fit-to-width follows the frame (window resize, phone rotation).\n  useEffect(() => {\n    const el = containerRef.current;\n    if (!el) return;\n    const observer = new ResizeObserver((entries) => {\n      const width = entries[0]?.contentRect.width ?? 0;\n      setFrameWidth(Math.max(0, Math.floor(width) - GUTTER * 2));\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n\n  const pageNumbers = doc ? Array.from({ length: doc.numPages }, (_, i) => i + 1) : [];\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"h-full w-full overflow-auto\"\n      role=\"document\"\n      aria-label={label}\n      aria-busy={status === \"loading\"}\n    >\n      {status === \"loading\" ? (\n        <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n          Loading…\n        </div>\n      ) : null}\n      {doc && frameWidth > 0 ? (\n        <div className=\"flex flex-col items-center gap-4 px-4 py-4\">\n          {pageNumbers.map((pageNumber) => (\n            <PdfPage\n              key={pageNumber}\n              doc={doc}\n              pageNumber={pageNumber}\n              fitWidth={frameWidth}\n              zoom={zoom}\n            />\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction PdfPage({\n  doc,\n  pageNumber,\n  fitWidth,\n  zoom,\n}: {\n  doc: PDFDocumentProxy;\n  pageNumber: number;\n  fitWidth: number;\n  zoom: number;\n}) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    let cancelled = false;\n    let renderTask: RenderTask | null = null;\n    doc\n      .getPage(pageNumber)\n      .then((page) => {\n        const canvas = canvasRef.current;\n        if (cancelled || !canvas) return;\n        const base = page.getViewport({ scale: 1 });\n        const scale = (fitWidth / base.width) * zoom;\n        const viewport = page.getViewport({ scale });\n        const dpr = typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n        canvas.width = Math.floor(viewport.width * dpr);\n        canvas.height = Math.floor(viewport.height * dpr);\n        canvas.style.width = `${Math.floor(viewport.width)}px`;\n        canvas.style.height = `${Math.floor(viewport.height)}px`;\n        const context = canvas.getContext(\"2d\");\n        if (!context) return;\n        renderTask = page.render({\n          canvas,\n          canvasContext: context,\n          viewport,\n          transform: dpr === 1 ? undefined : [dpr, 0, 0, dpr, 0, 0],\n        });\n        // A cancelled render (zoom changed mid-draw) rejects by design.\n        renderTask.promise.catch(() => undefined);\n      })\n      .catch(() => undefined);\n    return () => {\n      cancelled = true;\n      renderTask?.cancel();\n    };\n  }, [doc, pageNumber, fitWidth, zoom]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className=\"max-w-none bg-white shadow-sm ring-1 ring-foreground/10\"\n      aria-label={`Page ${pageNumber}`}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/pdf-pages.tsx"
    }
  ],
  "categories": [
    "patterns"
  ],
  "type": "registry:component"
}