{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdf-source",
  "title": "PDF source adapter",
  "description": "Bridges the document-source model to the PDF viewer: pdfAnchorToTarget and renderPdfSourceOverlay for PDF bbox anchors.",
  "registryDependencies": [
    "@retab/pdf-viewer",
    "@retab/document-source"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/pdf-source.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type {\n  Source,\n  SourceAnchor,\n  SourceLocation,\n} from \"@/lib/document-source\";\nimport {\n  PdfHighlight,\n  type PageOverlayProps,\n  type PdfViewerHandle,\n} from \"@/components/ui/pdf-viewer\";\n\nexport interface SourceTarget {\n  scrollTo?: (source: Source, options: { behavior: ScrollBehavior }) => void;\n}\n\n/**\n * Turn a source anchor into a PDF target, or `undefined` when the anchor isn't\n * a page region (a spreadsheet cell, a text span, …). PDF/image bbox anchors are\n * normalized [0, 1]; the viewer wants percentages, so scale by 100. An image\n * lands on the single page 1.\n */\nexport function pdfAnchorToTarget(\n  anchor: SourceAnchor,\n): SourceLocation | undefined {\n  if (anchor.kind === \"pdf_bbox\") {\n    if (!isPositiveInteger(anchor.page) || !isValidNormalizedBox(anchor)) {\n      return undefined;\n    }\n    return {\n      page: anchor.page,\n      area: {\n        left: anchor.left * 100,\n        top: anchor.top * 100,\n        width: anchor.width * 100,\n        height: anchor.height * 100,\n      },\n    };\n  }\n  if (anchor.kind === \"image_bbox\") {\n    if (anchor.page != null && !isPositiveInteger(anchor.page)) {\n      return undefined;\n    }\n    if (!isValidNormalizedBox(anchor)) return undefined;\n    return {\n      page: 1,\n      area: {\n        left: anchor.left * 100,\n        top: anchor.top * 100,\n        width: anchor.width * 100,\n        height: anchor.height * 100,\n      },\n    };\n  }\n  return undefined;\n}\n\nfunction isPositiveInteger(value: number): boolean {\n  return Number.isInteger(value) && value >= 1;\n}\n\nfunction isValidNormalizedBox({\n  left,\n  top,\n  width,\n  height,\n}: {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n}): boolean {\n  return (\n    Number.isFinite(left) &&\n    Number.isFinite(top) &&\n    Number.isFinite(width) &&\n    Number.isFinite(height) &&\n    left >= 0 &&\n    top >= 0 &&\n    width > 0 &&\n    height > 0 &&\n    left + width <= 1 &&\n    top + height <= 1\n  );\n}\n\n/**\n * A stable source target over a `PdfViewer` ref. The ref is read lazily, so this\n * is just the bridge from source anchors to the viewer's imperative handle.\n */\nexport function usePdfSourceTarget(\n  viewerRef: React.RefObject<PdfViewerHandle | null>,\n): SourceTarget {\n  return React.useMemo<SourceTarget>(\n    () => ({\n      scrollTo: (source: Source, options) => {\n        const target = pdfAnchorToTarget(source.anchor);\n        if (target) {\n          viewerRef.current?.scrollToPageArea(\n            {\n              pageNumber: target.page,\n              left: target.area.left,\n              top: target.area.top,\n              width: target.area.width,\n              height: target.area.height,\n            },\n            options,\n          );\n        }\n      },\n    }),\n    [viewerRef],\n  );\n}\n\n/**\n * Build a `renderPageOverlay` callback that draws a source highlight on its\n * page.\n */\nexport function renderPdfSourceOverlay(\n  source: Source | undefined,\n): (props: PageOverlayProps) => React.ReactNode {\n  const target = source ? pdfAnchorToTarget(source.anchor) : undefined;\n  return function PdfSourceOverlay({ pageNumber }: PageOverlayProps) {\n    return target && target.page === pageNumber ? (\n      <PdfHighlight area={target.area} />\n    ) : null;\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/pdf-source.tsx"
    }
  ],
  "type": "registry:ui"
}