{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "source-segmented-document",
  "title": "Source segmented document",
  "description": "Projection helpers and overlays that convert PDF/image bbox document sources into segmented-document segments, anchors, and active source highlights.",
  "registryDependencies": [
    "@retab/document-source",
    "@retab/source-field-link",
    "@retab/image-source",
    "@retab/image-viewer",
    "@retab/pdf-viewer",
    "@retab/segmented-document"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/source-segmented-document-model.ts",
      "content": "import type { Source, SourceAnchor, SourceMap } from \"@/lib/document-source\";\nimport {\n  buildColorMap,\n  segmentDisplayLabel,\n  segmentsPageCount,\n} from \"@/lib/segments\";\n\nimport {\n  createSegmentedDocumentModel,\n  type DocumentSegment,\n  type SegmentAnchor,\n  type SegmentedDocumentModel,\n} from \"./segmented-document-model\";\n\nexport type SegmentedSourceField = {\n  id: string;\n  label: string;\n  source?: Source | null;\n};\n\nexport type SourcesSegmentedDocumentInput =\n  | readonly SegmentedSourceField[]\n  | {\n      labels?: Record<string, string>;\n      sourceMap: SourceMap;\n    };\n\nexport function createSourcesSegmentedDocumentModel(\n  input: SourcesSegmentedDocumentInput,\n): SegmentedDocumentModel {\n  const fields = isSegmentedSourceFieldList(input)\n    ? input\n    : Object.entries(input.sourceMap).map(([id, source]) => ({\n        id,\n        label: input.labels?.[id] ?? (id || \"Value\"),\n        source,\n      }));\n  const colors = buildColorMap(fields.map((field) => field.label));\n  const segments: DocumentSegment[] = [];\n  const anchors: SegmentAnchor[] = [];\n\n  fields.forEach((field, index) => {\n    const label = sourceSegmentLabel(field);\n    const segment: DocumentSegment = {\n      id: sourceSegmentId(field.id, index),\n      label,\n      pages: [],\n      color:\n        colors.get(label) ??\n        colors.get(segmentDisplayLabel(label)) ??\n        \"var(--color-muted-foreground)\",\n      index,\n      sourceId: field.id,\n    };\n    const anchor = sourceToSegmentAnchor(field.source, segment.id);\n    if (anchor) {\n      segment.pages = [anchor.pageNumber];\n      anchors.push(anchor);\n    }\n    segments.push(segment);\n  });\n\n  return createSegmentedDocumentModel({\n    anchors,\n    pageCount: segmentsPageCount(segments),\n    segments,\n  });\n}\n\nfunction isSegmentedSourceFieldList(\n  input: SourcesSegmentedDocumentInput,\n): input is readonly SegmentedSourceField[] {\n  return Array.isArray(input);\n}\n\nexport function sourceToSegmentAnchor(\n  source: Source | null | undefined,\n  segmentId: string,\n): SegmentAnchor | null {\n  if (!source) return null;\n\n  switch (source.anchor.kind) {\n    case \"pdf_bbox\":\n      return bboxSourceAnchorToSegmentAnchor({\n        anchor: source.anchor,\n        pageNumber: source.anchor.page,\n        segmentId,\n      });\n    case \"image_bbox\":\n      return bboxSourceAnchorToSegmentAnchor({\n        anchor: source.anchor,\n        pageNumber: source.anchor.page ?? 1,\n        segmentId,\n      });\n    case \"csv_cell\":\n    case \"spreadsheet_cell\":\n    case \"docx_text_span\":\n    case \"docx_table_cell\":\n    case \"text_span\":\n      return null;\n  }\n}\n\nfunction bboxSourceAnchorToSegmentAnchor({\n  anchor,\n  pageNumber,\n  segmentId,\n}: {\n  anchor: Extract<SourceAnchor, { kind: \"pdf_bbox\" | \"image_bbox\" }>;\n  pageNumber: number;\n  segmentId: string;\n}): SegmentAnchor | null {\n  if (!Number.isInteger(pageNumber) || pageNumber < 1) return null;\n  if (!isValidNormalizedBox(anchor)) return null;\n\n  return {\n    id: `${segmentId}:anchor`,\n    segmentId,\n    pageNumber,\n    bounds: {\n      x: anchor.left,\n      y: anchor.top,\n      width: anchor.width,\n      height: anchor.height,\n    },\n  };\n}\n\nfunction sourceSegmentLabel(field: SegmentedSourceField): string {\n  return segmentDisplayLabel(field.label || field.id);\n}\n\nfunction sourceSegmentId(id: string, index: number): string {\n  return `source:${id || \"value\"}:${index}`;\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}) {\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",
      "type": "registry:ui",
      "target": "@ui/source-segmented-document-model.ts"
    },
    {
      "path": "registry/new-york-v4/ui/source-segmented-document-overlays.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { SegmentedSourceFieldLink } from \"@/components/ui/source-field-link\";\nimport { rotateImageArea } from \"@/components/ui/image-source\";\nimport type {\n  ImageFrameOverlayProps,\n  ImageViewerHandle,\n} from \"@/components/ui/image-viewer\";\nimport {\n  PdfHighlight,\n  type PageOverlayProps,\n  type PdfViewerHandle,\n} from \"@/components/ui/pdf-viewer\";\nimport type { SegmentAnchor } from \"@/components/ui/segmented-document-model\";\nimport { useSegmentedDocumentViewport } from \"@/components/ui/segmented-document-provider\";\n\nconst SOURCE_HIGHLIGHT_CLASS =\n  \"pointer-events-none absolute z-10 rounded-[2px] border border-blue-500/80 bg-blue-500/15 shadow-[0_4px_16px_rgb(37_99_235_/_18%)]\";\n\ntype AnchorWithBounds = SegmentAnchor & {\n  bounds: NonNullable<SegmentAnchor[\"bounds\"]>;\n};\n\ntype SegmentedSourceLink = Pick<SegmentedSourceFieldLink, \"activeAnchors\">;\n\nfunction activeAnchorsForPage(\n  anchors: readonly SegmentAnchor[],\n  pageNumber: number,\n): AnchorWithBounds[] {\n  return anchors.filter(\n    (anchor): anchor is AnchorWithBounds =>\n      anchor.bounds != null && anchor.pageNumber === pageNumber,\n  );\n}\n\nexport function useSegmentedPdfViewerHandle() {\n  const segmentedViewport = useSegmentedDocumentViewport();\n\n  return React.useCallback(\n    (handle: PdfViewerHandle | null) => {\n      segmentedViewport.documentHandlers.setDocumentHandle(handle);\n    },\n    [segmentedViewport.documentHandlers],\n  );\n}\n\nexport function useSegmentedImageViewerHandle() {\n  const segmentedViewport = useSegmentedDocumentViewport();\n\n  return React.useCallback(\n    (handle: ImageViewerHandle | null) => {\n      segmentedViewport.documentHandlers.setDocumentHandle(\n        handle\n          ? {\n              getViewportElement: handle.getViewportElement,\n              scrollToPage: (pageNumber, options) => {\n                handle.scrollToFrameArea(pageNumber, { top: 0 }, options);\n              },\n              scrollToPageArea: (target, options) => {\n                handle.scrollToFrameArea(\n                  target.pageNumber,\n                  {\n                    left: target.left,\n                    top: target.top,\n                    width: target.width,\n                    height: target.height,\n                  },\n                  options,\n                );\n              },\n            }\n          : null,\n      );\n    },\n    [segmentedViewport.documentHandlers],\n  );\n}\n\nexport function useSegmentedPdfSourceOverlay(link: SegmentedSourceLink) {\n  return React.useCallback(\n    ({ pageNumber }: PageOverlayProps) => {\n      const anchors = activeAnchorsForPage(link.activeAnchors, pageNumber);\n      if (anchors.length === 0) return null;\n\n      return (\n        <>\n          {anchors.map((anchor) => (\n            <PdfHighlight\n              key={anchor.id}\n              className={SOURCE_HIGHLIGHT_CLASS}\n              area={{\n                left: anchor.bounds.x * 100,\n                top: anchor.bounds.y * 100,\n                width: anchor.bounds.width * 100,\n                height: anchor.bounds.height * 100,\n              }}\n            />\n          ))}\n        </>\n      );\n    },\n    [link.activeAnchors],\n  );\n}\n\nexport function useSegmentedImageSourceOverlay(link: SegmentedSourceLink) {\n  return React.useCallback(\n    ({ frameNumber, rotation }: ImageFrameOverlayProps) => {\n      const anchors = activeAnchorsForPage(link.activeAnchors, frameNumber);\n      if (anchors.length === 0) return null;\n\n      return (\n        <>\n          {anchors.map((anchor) => {\n            const area = rotateImageArea(\n              {\n                left: anchor.bounds.x * 100,\n                top: anchor.bounds.y * 100,\n                width: anchor.bounds.width * 100,\n                height: anchor.bounds.height * 100,\n              },\n              rotation,\n            );\n\n            return (\n              <div\n                key={anchor.id}\n                className={SOURCE_HIGHLIGHT_CLASS}\n                style={{\n                  left: `${area.left}%`,\n                  top: `${area.top}%`,\n                  width: `${area.width}%`,\n                  height: `${area.height}%`,\n                }}\n              />\n            );\n          })}\n        </>\n      );\n    },\n    [link.activeAnchors],\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/source-segmented-document-overlays.tsx"
    }
  ],
  "type": "registry:ui"
}