{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "signature-pdf-viewer", "type": "registry:component", "title": "Signature PDF Viewer", "description": "react-pdf preview with text-anchor highlighting and manual click-to-place signature rectangles.", "registryDependencies": [ "badge", "button" ], "dependencies": [ "@signature-kit/pdf", "effect@4.0.0-beta.86", "lucide-react", "react-pdf@10.4.1" ], "files": [ { "path": "registry/default/signature-pdf-viewer/signature-pdf-viewer.tsx", "type": "registry:component", "target": "@components/signature-kit/signature-pdf-viewer.tsx", "content": "\"use client\";\n\nimport { findPdfTextAnchors } from \"@signature-kit/pdf/anchors\";\nimport { liteParseWorkerBrowserLayer } from \"@signature-kit/pdf/liteparse-browser\";\nimport {\n DEFAULT_PDF_ANCHOR_STAMP_SIZE,\n type PdfSignaturePage,\n type PdfSignatureRect,\n type PdfStampSize,\n pdfTextAnchorMatchersFromProps,\n} from \"@signature-kit/pdf/config\";\nimport { Effect, Exit, Result } from \"effect\";\nimport { Loader2, LocateFixed } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Document, Page, pdfjs } from \"react-pdf\";\nimport \"react-pdf/dist/Page/AnnotationLayer.css\";\nimport \"react-pdf/dist/Page/TextLayer.css\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nif (typeof window !== \"undefined\") {\n pdfjs.GlobalWorkerOptions.workerSrc = `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;\n}\n\nexport type SignaturePdfViewerMode = \"anchors\" | \"manual\";\n\nexport type SignaturePdfViewerProps = {\n readonly file: Uint8Array;\n readonly pages: ReadonlyArray;\n readonly mode: SignaturePdfViewerMode;\n readonly value: ReadonlyArray;\n readonly onChange: (rects: ReadonlyArray) => void;\n readonly anchorTokens?: ReadonlyArray;\n readonly anchorDigits?: ReadonlyArray;\n readonly stampSize?: PdfStampSize;\n readonly pageWidth?: number;\n readonly className?: string;\n};\n\nconst emptyAnchorValues: ReadonlyArray = [];\n\ntype ActiveAnchorScan = {\n readonly owner: number;\n cancel: () => void;\n};\n\nconst clamp = (value: number, min: number, max: number): number =>\n Math.min(max, Math.max(min, value));\n\nexport function SignaturePdfViewer({\n file,\n pages,\n mode,\n value,\n onChange,\n anchorTokens = emptyAnchorValues,\n anchorDigits = emptyAnchorValues,\n stampSize = DEFAULT_PDF_ANCHOR_STAMP_SIZE,\n pageWidth = 720,\n className,\n}: SignaturePdfViewerProps) {\n const [anchorStatus, setAnchorStatus] = React.useState<\"idle\" | \"scanning\" | \"error\">(\"idle\");\n const activeAnchorScan = React.useRef(null);\n const anchorScanOwner = React.useRef(0);\n const matchers = React.useMemo(\n () => pdfTextAnchorMatchersFromProps(anchorTokens, anchorDigits),\n [anchorDigits, anchorTokens],\n );\n const anchorScanInputs = React.useMemo(\n () => ({ file, matchers, mode, pages, stampSize }),\n [file, matchers, mode, pages, stampSize],\n );\n const handledAnchorScanInputs = React.useRef(anchorScanInputs);\n const documentFile = React.useMemo(() => {\n const buffer = new ArrayBuffer(file.byteLength);\n new Uint8Array(buffer).set(file);\n return { data: buffer };\n }, [file]);\n const invalidateAnchorScan = React.useCallback((): void => {\n const activeScan = activeAnchorScan.current;\n anchorScanOwner.current += 1;\n activeAnchorScan.current = null;\n activeScan?.cancel();\n }, []);\n\n React.useEffect(() => {\n if (handledAnchorScanInputs.current === anchorScanInputs) return;\n handledAnchorScanInputs.current = anchorScanInputs;\n invalidateAnchorScan();\n setAnchorStatus(\"idle\");\n }, [anchorScanInputs, invalidateAnchorScan]);\n\n React.useEffect(() => invalidateAnchorScan, [invalidateAnchorScan]);\n\n const scanAnchors = (): void => {\n invalidateAnchorScan();\n handledAnchorScanInputs.current = anchorScanInputs;\n if (matchers.length === 0) {\n setAnchorStatus(\"idle\");\n return;\n }\n\n const owner = anchorScanOwner.current + 1;\n const scan: ActiveAnchorScan = { owner, cancel: () => {} };\n anchorScanOwner.current = owner;\n activeAnchorScan.current = scan;\n setAnchorStatus(\"scanning\");\n const cancel = Effect.runCallback(\n findPdfTextAnchors({\n pdf: file,\n pages,\n matchers,\n stampSize,\n }).pipe(Effect.provide(liteParseWorkerBrowserLayer), Effect.result),\n {\n onExit: (exit) => {\n if (anchorScanOwner.current !== owner || activeAnchorScan.current !== scan) return;\n activeAnchorScan.current = null;\n if (Exit.isSuccess(exit) && Result.isSuccess(exit.value)) {\n setAnchorStatus(\"idle\");\n onChange(exit.value.success);\n return;\n }\n setAnchorStatus(\"error\");\n },\n },\n );\n\n if (activeAnchorScan.current === scan) scan.cancel = cancel;\n };\n\n const placeManually = (\n event: React.PointerEvent,\n page: PdfSignaturePage,\n ): void => {\n if (mode !== \"manual\") return;\n const bounds = event.currentTarget.getBoundingClientRect();\n const scale = bounds.width / page.width;\n const pageX = (event.clientX - bounds.left) / scale;\n const pageY = (event.clientY - bounds.top) / scale;\n const rect: PdfSignatureRect = {\n pageIndex: page.index,\n x: clamp(pageX - stampSize.width / 2, 0, page.width - stampSize.width),\n y: clamp(pageY - stampSize.height / 2, 0, page.height - stampSize.height),\n width: stampSize.width,\n height: stampSize.height,\n };\n onChange([...value.filter((candidate) => candidate.pageIndex !== page.index), rect]);\n };\n\n return (\n
\n
\n
\n \n {mode === \"anchors\" ? \"Anchor placement\" : \"Manual placement\"}\n \n {value.length} placement(s)\n
\n {mode === \"anchors\" ? (\n \n {anchorStatus === \"scanning\" ? (\n \n ) : (\n \n )}\n Find anchors\n \n ) : null}\n
\n {anchorStatus === \"error\" ? (\n

\n No matching anchor could be read from this PDF.\n

\n ) : null}\n
\n Loading PDF…

}\n >\n
\n {pages.map((page) => {\n const scale = pageWidth / page.width;\n const pageRects = value.filter((rect) => rect.pageIndex === page.index);\n return (\n placeManually(event, page)}\n >\n \n {pageRects.map((rect) => (\n \n \n signature\n \n
\n ))}\n
\n );\n })}\n
\n \n \n \n );\n}\n" } ] }