{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "layout-blocks",
  "title": "Layout Blocks",
  "description": "A provider-agnostic layout inspection component for OCR output: normalize Google Document AI, AWS Textract, or Azure Document Intelligence into shared blocks, lines, and words, then review them over the source page with virtualized rows, confidence filtering, and precise overlays.",
  "registryDependencies": [
    "@retab/document-evidence",
    "@retab/file-viewer",
    "@retab/interactive-item-list",
    "@retab/pdf-viewer",
    "@retab/scroll-area",
    "@retab/segmented-document",
    "@retab/utils"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/layout-blocks.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  FileViewer,\n  FileViewerContent,\n  FileViewerProvider,\n  FileViewerInset,\n  FileViewerSidebar,\n  FileViewerSidebarTrigger,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport {\n  PdfViewerPages,\n  PdfViewerProvider,\n  type PdfDocumentSource,\n  type PdfViewerHandle,\n} from \"@/components/ui/pdf-viewer\";\nimport {\n  SegmentedDocumentProvider,\n  useSegmentedDocumentViewport,\n} from \"@/components/ui/segmented-document-provider\";\nimport { useSegmentedItemLink } from \"@/components/ui/segmented-item-link\";\nimport {\n  ViewerHeader,\n  ViewerRoot,\n  ViewerSidebarTrigger,\n} from \"@/components/ui/viewer\";\n\nimport {\n  azureToLayoutDocument,\n  type AzureDocument,\n} from \"./layout-blocks-azure\";\nimport {\n  documentAiPageImages,\n  documentAiToLayoutDocument,\n  type DocumentAiDocument,\n} from \"./layout-blocks-document-ai\";\nimport { createLayoutBlocksViewerModel } from \"./layout-blocks-model\";\nimport { LayoutBlocksPanel } from \"./layout-blocks-panel\";\nimport { layoutDocumentToPdfBlob } from \"./layout-blocks-pdf\";\nimport { createOcrSegmentedDocumentModel } from \"./layout-blocks-segmented-document-model\";\nimport {\n  textractToLayoutDocument,\n  type TextractDocument,\n} from \"./layout-blocks-textract\";\nimport type { LayoutDocument, LayoutLevel } from \"./layout-blocks-types\";\nimport { LayoutOverlayLayer } from \"./layout-overlay-layer\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nconst LOW_CONFIDENCE_THRESHOLD = 0.9;\nconst LEVEL_ORDER: LayoutLevel[] = [\"block\", \"paragraph\", \"line\", \"word\"];\nconst EMPTY_PAGE_IMAGES: ReadonlyMap<number, string> = new Map();\n\n/**\n * Discriminated source for the OCR viewer. Each provider's raw output is\n * normalized to a shared {@link LayoutDocument} before rendering, so the viewer\n * itself is provider-agnostic.\n */\nexport type OcrSource =\n  | {\n      provider: \"google-document-ai\";\n      output: DocumentAiDocument;\n      pageImages?: ReadonlyMap<number, string>;\n    }\n  | {\n      provider: \"aws-textract\";\n      output: TextractDocument;\n      pageImages?: ReadonlyMap<number, string>;\n    }\n  | {\n      provider: \"azure-document-intelligence\";\n      output: AzureDocument;\n      pageImages?: ReadonlyMap<number, string>;\n    };\n\nexport function OcrLayoutBlocks({\n  className,\n  heightClassName = \"h-[680px]\",\n  source,\n}: {\n  className?: string;\n  heightClassName?: string;\n  source: OcrSource;\n}) {\n  const { document: layoutDocument, pageImages } = React.useMemo(\n    () => normalizeOcrSource(source),\n    [source],\n  );\n  const inspectedLevels = React.useMemo(\n    () => inspectedLevelsForDocument(layoutDocument),\n    [layoutDocument],\n  );\n  const pdfSource = useLayoutPdfSource(layoutDocument, pageImages);\n  const fallbackPageSize = layoutDocument.pages[0]\n    ? {\n        width: layoutDocument.pages[0].width,\n        height: layoutDocument.pages[0].height,\n      }\n    : undefined;\n  const [lowConfidenceOnly, setLowConfidenceOnly] = React.useState(false);\n  const model = React.useMemo(\n    () =>\n      createLayoutBlocksViewerModel({\n        document: layoutDocument,\n        levels: inspectedLevels,\n        lowConfidenceOnly,\n        threshold: LOW_CONFIDENCE_THRESHOLD,\n      }),\n    [layoutDocument, inspectedLevels, lowConfidenceOnly],\n  );\n  const segmentedDocumentModel = React.useMemo(\n    () =>\n      createOcrSegmentedDocumentModel({\n        document: layoutDocument,\n        items: model.visibleItems,\n      }),\n    [layoutDocument, model.visibleItems],\n  );\n\n  return (\n    <SegmentedDocumentProvider model={segmentedDocumentModel}>\n      <OcrLayoutBlocksContent\n        className={className}\n        fallbackPageSize={fallbackPageSize}\n        heightClassName={heightClassName}\n        inspectedLevels={inspectedLevels}\n        lowConfidenceOnly={lowConfidenceOnly}\n        model={model}\n        pdfSource={pdfSource}\n        setLowConfidenceOnly={setLowConfidenceOnly}\n      />\n    </SegmentedDocumentProvider>\n  );\n}\n\n/**\n * Back-compatible entry point for Google Document AI output. Prefer\n * {@link OcrLayoutBlocks} with an explicit {@link OcrSource} for new code.\n */\nexport function DocumentAiLayoutBlocks({\n  className,\n  heightClassName,\n  output,\n}: {\n  className?: string;\n  heightClassName?: string;\n  output: DocumentAiDocument;\n}) {\n  const source = React.useMemo<OcrSource>(\n    () => ({ provider: \"google-document-ai\", output }),\n    [output],\n  );\n  return (\n    <OcrLayoutBlocks\n      className={className}\n      heightClassName={heightClassName}\n      source={source}\n    />\n  );\n}\n\nfunction OcrLayoutBlocksContent({\n  className,\n  fallbackPageSize,\n  heightClassName,\n  inspectedLevels,\n  lowConfidenceOnly,\n  model,\n  pdfSource,\n  setLowConfidenceOnly,\n}: {\n  className?: string;\n  fallbackPageSize?: { width: number; height: number };\n  heightClassName: string;\n  inspectedLevels: readonly LayoutLevel[];\n  lowConfidenceOnly: boolean;\n  model: ReturnType<typeof createLayoutBlocksViewerModel>;\n  pdfSource: ReturnType<typeof useLayoutPdfSource>;\n  setLowConfidenceOnly: React.Dispatch<React.SetStateAction<boolean>>;\n}) {\n  const segmentedViewport = useSegmentedDocumentViewport();\n  const itemLink = useSegmentedItemLink();\n  const {\n    activeItemId,\n    clearPreview,\n    navigateItem,\n    previewItem,\n    selectItem,\n    selectedItemId,\n  } = itemLink;\n\n  const renderPageOverlay = React.useCallback(\n    ({ pageNumber, rotation }: { pageNumber: number; rotation: number }) => {\n      const page = model.index.pagesByNumber.get(pageNumber);\n      if (!page) return null;\n\n      return (\n        <LayoutOverlayLayer\n          interactive\n          activeItemId={activeItemId}\n          items={model.visibleItems.filter(\n            (item) => item.pageNumber === pageNumber,\n          )}\n          page={page}\n          rotation={rotation}\n          selectedItemId={selectedItemId}\n          visibleLevels={inspectedLevels}\n          onItemClick={(item) => {\n            selectItem(item.id);\n            navigateItem(item.id, { behavior: \"smooth\", clearPreview: false });\n          }}\n          onItemPointerEnter={(item) => previewItem(item.id)}\n          onItemPointerLeave={clearPreview}\n        />\n      );\n    },\n    [\n      activeItemId,\n      clearPreview,\n      inspectedLevels,\n      model.index.pagesByNumber,\n      model.visibleItems,\n      navigateItem,\n      previewItem,\n      selectItem,\n      selectedItemId,\n    ],\n  );\n  const setPdfViewerHandle = React.useCallback(\n    (handle: PdfViewerHandle | null) => {\n      segmentedViewport.documentHandlers.setDocumentHandle(handle);\n    },\n    [segmentedViewport.documentHandlers],\n  );\n\n  const level = inspectedLevels[0] ?? \"block\";\n\n  // The header row is rendered in two hosts (inside the file-viewer shell\n  // once the PDF is ready, directly under the root while preparing), so the\n  // toggle trigger is injected per host: the shell's own trigger drives the\n  // motion kernel; the preparing state shows an inert placeholder trigger.\n  const renderHeader = (trigger: React.ReactNode) => (\n    <ViewerHeader>\n      <div className=\"flex items-center justify-between gap-3 p-3\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          {trigger}\n          <div className=\"flex min-w-0 items-center gap-2\">\n            <div className=\"truncate text-sm font-medium\">OCR</div>\n            <div className=\"text-muted-foreground shrink-0 text-xs\">\n              {levelCountLabel(level, model.visibleItems.length)}\n            </div>\n          </div>\n        </div>\n        <label className=\"text-muted-foreground flex shrink-0 items-center gap-2 text-xs\">\n          <input\n            type=\"checkbox\"\n            className=\"size-3.5\"\n            checked={lowConfidenceOnly}\n            onChange={(event) =>\n              setLowConfidenceOnly(event.currentTarget.checked)\n            }\n          />\n          Low confidence\n        </label>\n      </div>\n    </ViewerHeader>\n  );\n  const panel = (\n    <LayoutBlocksPanel\n      activeItemId={activeItemId}\n      className=\"min-h-0 flex-1\"\n      emptyLabel={\n        lowConfidenceOnly\n          ? `No low-confidence OCR ${levelPlural(level)} found.`\n          : `No OCR ${levelPlural(level)} found.`\n      }\n      items={model.evidenceItems}\n      selectedItemId={selectedItemId}\n      onActiveItemIdChange={previewItem}\n      onNavigateItem={(item, options) => {\n        navigateItem(item.id, {\n          behavior: options?.behavior,\n          clearPreview: options?.behavior === \"auto\" ? false : undefined,\n        });\n      }}\n      onSelectedItemIdChange={(itemId) => {\n        selectItem(itemId);\n      }}\n    />\n  );\n\n  // The blocks panel lives in the file-viewer shell's OWN sidebar: the shell's\n  // motion kernel is the single owner of sidebar toggles, and only its trigger\n  // choreographs the document slide (commit-then-relax). Hosting the panel in\n  // outer chrome instead leaves the pane resizing under the viewer, and the\n  // fit-width re-layout chasing it — the document drifts sideways, recenters,\n  // then resizes.\n  return (\n    <ViewerRoot\n      data-layout-blocks=\"\"\n      className={cn(\"bg-background\", heightClassName, className)}\n      sidebarSide=\"right\"\n    >\n      {pdfSource.source ? (\n        <FileViewerProvider source={pdfSource.source} defaultSidebarOpen>\n          <FileViewer className=\"bg-background\">\n            <PdfViewerProvider>\n              {renderHeader(<FileViewerSidebarTrigger className=\"-ml-1\" />)}\n              <FileViewerContent>\n                <FileViewerInset>\n                  <FileViewerViewport>\n                    <PdfViewerPages\n                      ref={setPdfViewerHandle}\n                      bare\n                      className=\"h-full\"\n                      fallbackPageSize={fallbackPageSize}\n                      onScrollProgressChange={\n                        segmentedViewport.documentHandlers\n                          .onScrollProgressChange\n                      }\n                      onVisiblePageChange={\n                        segmentedViewport.documentHandlers.onCurrentPageChange\n                      }\n                      renderPageOverlay={renderPageOverlay}\n                    />\n                  </FileViewerViewport>\n                </FileViewerInset>\n                <FileViewerSidebar\n                  aria-label=\"OCR blocks\"\n                  side=\"right\"\n                  width=\"320px\"\n                  className=\"bg-background flex min-h-0 flex-col border-l\"\n                >\n                  {panel}\n                </FileViewerSidebar>\n              </FileViewerContent>\n            </PdfViewerProvider>\n          </FileViewer>\n        </FileViewerProvider>\n      ) : (\n        <>\n          {renderHeader(<ViewerSidebarTrigger className=\"-ml-1\" disabled />)}\n          <div className=\"flex min-h-0 min-w-0 flex-1 overflow-hidden\">\n            <div className=\"bg-muted/20 text-muted-foreground grid min-w-0 flex-1 place-items-center p-6 text-sm\">\n              {pdfSource.error ?? \"Preparing OCR pages...\"}\n            </div>\n            <aside\n              aria-label=\"OCR blocks\"\n              className=\"bg-background flex w-[320px] min-w-0 shrink-0 flex-col border-l\"\n            >\n              {panel}\n            </aside>\n          </div>\n        </>\n      )}\n    </ViewerRoot>\n  );\n}\n\nfunction normalizeOcrSource(source: OcrSource): {\n  document: LayoutDocument;\n  pageImages: ReadonlyMap<number, string>;\n} {\n  switch (source.provider) {\n    case \"aws-textract\":\n      return {\n        document: textractToLayoutDocument(source.output),\n        pageImages: source.pageImages ?? EMPTY_PAGE_IMAGES,\n      };\n    case \"azure-document-intelligence\":\n      return {\n        document: azureToLayoutDocument(source.output),\n        pageImages: source.pageImages ?? EMPTY_PAGE_IMAGES,\n      };\n    case \"google-document-ai\":\n    default:\n      return {\n        document: documentAiToLayoutDocument(source.output),\n        pageImages: source.pageImages ?? documentAiPageImages(source.output),\n      };\n  }\n}\n\n/** Pick the coarsest level present so the viewer always has rows to inspect. */\nfunction inspectedLevelsForDocument(document: LayoutDocument): LayoutLevel[] {\n  for (const level of LEVEL_ORDER) {\n    if (document.items.some((item) => item.level === level)) return [level];\n  }\n  return [\"block\"];\n}\n\nfunction levelPlural(level: LayoutLevel): string {\n  switch (level) {\n    case \"block\":\n      return \"blocks\";\n    case \"paragraph\":\n      return \"paragraphs\";\n    case \"line\":\n      return \"lines\";\n    case \"word\":\n      return \"words\";\n  }\n}\n\nfunction levelCountLabel(level: LayoutLevel, count: number): string {\n  const plural = levelPlural(level);\n  const singular = plural.slice(0, -1);\n  return `${count} ${count === 1 ? singular : plural}`;\n}\n\nfunction useLayoutPdfSource(\n  document: LayoutDocument,\n  pageImages: ReadonlyMap<number, string>,\n) {\n  const [state, setState] = React.useState<{\n    error?: string;\n    source?: PdfDocumentSource;\n  }>({});\n\n  useKeyedMountEffect(joinEffectKey([document, pageImages]), () => {\n    let isCurrent = true;\n    setState({});\n\n    layoutDocumentToPdfBlob(document, new Map(pageImages))\n      .then((blob) => {\n        if (!isCurrent) return;\n        setState({\n          source: {\n            kind: \"blob\",\n            blob,\n            fileName: \"ocr-pages.pdf\",\n            identityKey: `ocr-pdf:${document.pages.length}:${pageImages.size}:${blob.size}`,\n            mimeType: \"application/pdf\",\n          },\n        });\n      })\n      .catch((error: unknown) => {\n        if (!isCurrent) return;\n        setState({\n          error:\n            error instanceof Error\n              ? error.message\n              : \"Failed to prepare OCR pages.\",\n        });\n      });\n\n    return () => {\n      isCurrent = false;\n    };\n  });\n\n  return state;\n}\n\nexport type {\n  LayoutItem,\n  LayoutLevel,\n  LayoutPage,\n} from \"./layout-blocks-types\";\nexport type { DocumentAiDocument } from \"./layout-blocks-document-ai\";\nexport type { TextractDocument } from \"./layout-blocks-textract\";\nexport type { AzureDocument } from \"./layout-blocks-azure\";\nexport {\n  documentAiPageImages,\n  documentAiToLayoutDocument,\n} from \"./layout-blocks-document-ai\";\nexport { textractToLayoutDocument } from \"./layout-blocks-textract\";\nexport { azureToLayoutDocument } from \"./layout-blocks-azure\";\nexport { documentAiToPdfBlob } from \"./layout-blocks-document-ai-pdf\";\nexport { layoutDocumentToPdfBlob } from \"./layout-blocks-pdf\";\nexport { createLayoutItemIndex } from \"./layout-blocks-index\";\nexport {\n  createLayoutBlocksViewerModel,\n  layoutItemToEvidenceItem,\n} from \"./layout-blocks-model\";\nexport { LayoutBlocksPanel } from \"./layout-blocks-panel\";\nexport { LayoutOverlayLayer } from \"./layout-overlay-layer\";\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-types.ts",
      "content": "export type LayoutLevel = \"block\" | \"paragraph\" | \"line\" | \"word\";\n\nexport type LayoutKind =\n  | \"title\"\n  | \"heading\"\n  | \"paragraph\"\n  | \"list\"\n  | \"table\"\n  | \"figure\"\n  | \"header\"\n  | \"footer\"\n  | \"pageNumber\"\n  | \"other\";\n\nexport type LayoutPoint = {\n  x: number;\n  y: number;\n};\n\nexport type LayoutRect = {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n};\n\nexport type LayoutQuad = [LayoutPoint, LayoutPoint, LayoutPoint, LayoutPoint];\n\nexport type LayoutTextSpan = {\n  start: number;\n  end: number;\n};\n\nexport type LayoutItem = {\n  id: string;\n  pageNumber: number;\n  level: LayoutLevel;\n  kind: LayoutKind;\n  text: string;\n  confidence?: number;\n  rect?: LayoutRect;\n  quad?: LayoutQuad;\n  parentId?: string;\n  span?: LayoutTextSpan;\n};\n\nexport type LayoutPage = {\n  pageNumber: number;\n  width: number;\n  height: number;\n  rotation: 0 | 90 | 180 | 270;\n};\n\nexport type LayoutDocument = {\n  text: string;\n  pages: LayoutPage[];\n  items: LayoutItem[];\n};\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-types.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-document-ai.ts",
      "content": "import { normalizeLayoutQuad, quadToRect } from \"./layout-blocks-geometry\";\nimport type {\n  LayoutDocument,\n  LayoutItem,\n  LayoutKind,\n  LayoutLevel,\n  LayoutPage,\n  LayoutPoint,\n  LayoutQuad,\n  LayoutTextSpan,\n} from \"./layout-blocks-types\";\n\ntype DocumentAiTextSegment = {\n  startIndex?: string;\n  endIndex?: string;\n};\n\ntype DocumentAiLayout = {\n  textAnchor?: {\n    textSegments?: DocumentAiTextSegment[];\n  };\n  confidence?: number;\n  boundingPoly?: {\n    vertices?: Array<{ x?: number; y?: number }>;\n    normalizedVertices?: Array<{ x?: number; y?: number }>;\n  };\n  orientation?: number;\n};\n\ntype DocumentAiLayoutNode = {\n  layout?: DocumentAiLayout;\n};\n\ntype DocumentAiPage = {\n  pageNumber?: number;\n  dimension?: {\n    width?: number;\n    height?: number;\n    unit?: string;\n  };\n  image?: {\n    content?: string;\n    mimeType?: string;\n    width?: number;\n    height?: number;\n  };\n  layout?: DocumentAiLayout;\n  blocks?: DocumentAiLayoutNode[];\n  paragraphs?: DocumentAiLayoutNode[];\n  lines?: DocumentAiLayoutNode[];\n  tokens?: DocumentAiLayoutNode[];\n};\n\nexport type DocumentAiDocument = {\n  text?: string;\n  pages?: DocumentAiPage[];\n  mimeType?: string;\n  uri?: string;\n};\n\nexport function documentAiToLayoutDocument(\n  input: DocumentAiDocument,\n): LayoutDocument {\n  const text = input.text ?? \"\";\n  const pages = (input.pages ?? []).flatMap((page, pageIndex) =>\n    documentAiPageToLayoutPage(page, pageIndex),\n  );\n  const pagesByNumber = new Map(pages.map((page) => [page.pageNumber, page]));\n  const items = (input.pages ?? []).flatMap((page, pageIndex) => {\n    const pageNumber = page.pageNumber ?? pageIndex + 1;\n    const layoutPage = pagesByNumber.get(pageNumber);\n    if (!layoutPage) return [];\n\n    const pageItems = [\n      ...documentAiNodesToLayoutItems({\n        nodes: page.blocks ?? [],\n        level: \"block\",\n        page: layoutPage,\n        text,\n      }),\n      ...documentAiNodesToLayoutItems({\n        nodes: page.paragraphs ?? [],\n        level: \"paragraph\",\n        page: layoutPage,\n        text,\n      }),\n      ...documentAiNodesToLayoutItems({\n        nodes: page.lines ?? [],\n        level: \"line\",\n        page: layoutPage,\n        text,\n      }),\n      ...documentAiNodesToLayoutItems({\n        nodes: page.tokens ?? [],\n        level: \"word\",\n        page: layoutPage,\n        text,\n      }),\n    ];\n\n    return assignDocumentAiParents(pageItems);\n  });\n\n  return { text, pages, items };\n}\n\nexport function documentAiPageToDataUrl(page: DocumentAiPage) {\n  if (!page.image?.content) return undefined;\n  return `data:${page.image.mimeType ?? \"image/png\"};base64,${\n    page.image.content\n  }`;\n}\n\n/** Map of page number → embedded page-image data URL, for the PDF backdrop. */\nexport function documentAiPageImages(\n  input: DocumentAiDocument,\n): Map<number, string> {\n  const images = new Map<number, string>();\n  (input.pages ?? []).forEach((page, index) => {\n    const dataUrl = documentAiPageToDataUrl(page);\n    if (dataUrl) images.set(page.pageNumber ?? index + 1, dataUrl);\n  });\n  return images;\n}\n\nfunction documentAiPageToLayoutPage(\n  page: DocumentAiPage,\n  pageIndex: number,\n): LayoutPage[] {\n  const pageNumber = page.pageNumber ?? pageIndex + 1;\n  const width = finitePositiveNumber(\n    page.dimension?.width ?? page.image?.width,\n  );\n  const height = finitePositiveNumber(\n    page.dimension?.height ?? page.image?.height,\n  );\n\n  if (!width || !height) return [];\n\n  return [\n    {\n      pageNumber,\n      width,\n      height,\n      rotation: documentAiOrientationToRotation(page.layout?.orientation),\n    },\n  ];\n}\n\nfunction documentAiNodesToLayoutItems({\n  nodes,\n  level,\n  page,\n  text,\n}: {\n  nodes: DocumentAiLayoutNode[];\n  level: LayoutLevel;\n  page: LayoutPage;\n  text: string;\n}): LayoutItem[] {\n  return nodes.flatMap((node, index) => {\n    const layout = node.layout;\n    if (!layout) return [];\n\n    const span = documentAiTextSpan(layout, text.length);\n    const nodeText = span ? text.slice(span.start, span.end) : \"\";\n    const quad = documentAiLayoutQuad(layout, page);\n    const normalizedQuad = normalizeLayoutQuad(quad, page);\n    if (!normalizedQuad) return [];\n\n    return {\n      id: documentAiLayoutItemId({ page, level, index, span }),\n      pageNumber: page.pageNumber,\n      level,\n      kind: documentAiLayoutKind(level, nodeText),\n      text: nodeText,\n      confidence: normalizeConfidence(layout.confidence),\n      quad: normalizedQuad,\n      rect: quadToRect(normalizedQuad),\n      span,\n    };\n  });\n}\n\nfunction documentAiTextSpan(\n  layout: DocumentAiLayout,\n  textLength: number,\n): LayoutTextSpan | undefined {\n  const segments = layout.textAnchor?.textSegments ?? [];\n  if (!segments.length) return undefined;\n\n  let start = textLength;\n  let end = 0;\n  for (const segment of segments) {\n    const segmentStart = parseTextIndex(segment.startIndex, 0);\n    const segmentEnd = parseTextIndex(segment.endIndex, segmentStart);\n    start = Math.min(start, segmentStart);\n    end = Math.max(end, segmentEnd);\n  }\n\n  start = clampTextIndex(start, textLength);\n  end = clampTextIndex(end, textLength);\n\n  return end >= start ? { start, end } : undefined;\n}\n\nfunction parseTextIndex(value: string | undefined, fallback: number) {\n  if (value == null) return fallback;\n  const parsed = Number(value);\n  return Number.isFinite(parsed) ? parsed : fallback;\n}\n\nfunction clampTextIndex(value: number, textLength: number) {\n  return Math.min(textLength, Math.max(0, Math.trunc(value)));\n}\n\nfunction documentAiLayoutQuad(\n  layout: DocumentAiLayout,\n  page: LayoutPage,\n): LayoutQuad | undefined {\n  const vertices = layout.boundingPoly?.vertices;\n  if (vertices && vertices.length >= 4) {\n    return vertices.slice(0, 4).map((vertex) => ({\n      x: vertex.x ?? 0,\n      y: vertex.y ?? 0,\n    })) as LayoutQuad;\n  }\n\n  const normalizedVertices = layout.boundingPoly?.normalizedVertices;\n  if (normalizedVertices && normalizedVertices.length >= 4) {\n    return normalizedVertices.slice(0, 4).map((vertex) => ({\n      x: (vertex.x ?? 0) * page.width,\n      y: (vertex.y ?? 0) * page.height,\n    })) as LayoutQuad;\n  }\n}\n\nfunction documentAiLayoutItemId({\n  page,\n  level,\n  index,\n  span,\n}: {\n  page: LayoutPage;\n  level: LayoutLevel;\n  index: number;\n  span?: LayoutTextSpan;\n}) {\n  const spanKey = span ? `${span.start}-${span.end}` : \"no-span\";\n  return `document-ai:p${page.pageNumber}:${level}:${index}:${spanKey}`;\n}\n\nfunction documentAiLayoutKind(level: LayoutLevel, text: string): LayoutKind {\n  const trimmedText = text.trim();\n  if (/^\\d+$/.test(trimmedText)) return \"pageNumber\";\n  if (level === \"paragraph\" || level === \"block\") return \"paragraph\";\n  return \"other\";\n}\n\nfunction documentAiOrientationToRotation(\n  orientation: number | undefined,\n): 0 | 90 | 180 | 270 {\n  switch (orientation) {\n    case 2:\n      return 90;\n    case 3:\n      return 180;\n    case 4:\n      return 270;\n    case 1:\n    default:\n      return 0;\n  }\n}\n\nfunction normalizeConfidence(confidence: number | undefined) {\n  if (confidence == null || !Number.isFinite(confidence)) return undefined;\n  return Math.min(1, Math.max(0, confidence));\n}\n\nfunction finitePositiveNumber(value: number | undefined) {\n  if (value == null || !Number.isFinite(value)) return undefined;\n  return Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nfunction assignDocumentAiParents(items: LayoutItem[]) {\n  const blocks = items.filter((item) => item.level === \"block\");\n  const paragraphs = items.filter((item) => item.level === \"paragraph\");\n  const lines = items.filter((item) => item.level === \"line\");\n\n  return items.map((item) => {\n    if (item.level === \"paragraph\") {\n      return {\n        ...item,\n        parentId: findSmallestContainingSpan(item, blocks)?.id,\n      };\n    }\n    if (item.level === \"line\") {\n      return {\n        ...item,\n        parentId: findSmallestContainingSpan(item, paragraphs)?.id,\n      };\n    }\n    if (item.level === \"word\") {\n      return { ...item, parentId: findSmallestContainingSpan(item, lines)?.id };\n    }\n    return item;\n  });\n}\n\nfunction findSmallestContainingSpan(\n  item: LayoutItem,\n  candidates: LayoutItem[],\n): LayoutItem | undefined {\n  if (!item.span) return undefined;\n  let parent: LayoutItem | undefined;\n  let parentLength = Number.POSITIVE_INFINITY;\n\n  for (const candidate of candidates) {\n    if (!candidate.span) continue;\n    if (\n      candidate.span.start > item.span.start ||\n      candidate.span.end < item.span.end\n    ) {\n      continue;\n    }\n\n    const candidateLength = candidate.span.end - candidate.span.start;\n    if (candidateLength < parentLength) {\n      parent = candidate;\n      parentLength = candidateLength;\n    }\n  }\n\n  return parent;\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-document-ai.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-document-ai-pdf.ts",
      "content": "import {\n  documentAiPageImages,\n  documentAiToLayoutDocument,\n  type DocumentAiDocument,\n} from \"./layout-blocks-document-ai\";\nimport { layoutDocumentToPdfBlob } from \"./layout-blocks-pdf\";\n\nexport async function documentAiToPdfBlob(\n  document: DocumentAiDocument,\n): Promise<Blob> {\n  return layoutDocumentToPdfBlob(\n    documentAiToLayoutDocument(document),\n    documentAiPageImages(document),\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-document-ai-pdf.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-textract.ts",
      "content": "import { normalizeLayoutQuad, quadToRect } from \"./layout-blocks-geometry\";\nimport type {\n  LayoutDocument,\n  LayoutItem,\n  LayoutKind,\n  LayoutLevel,\n  LayoutPage,\n  LayoutQuad,\n} from \"./layout-blocks-types\";\n\ntype TextractPoint = {\n  X?: number;\n  Y?: number;\n};\n\ntype TextractBoundingBox = {\n  Width?: number;\n  Height?: number;\n  Left?: number;\n  Top?: number;\n};\n\ntype TextractGeometry = {\n  BoundingBox?: TextractBoundingBox;\n  Polygon?: TextractPoint[];\n};\n\ntype TextractRelationship = {\n  Type?: string;\n  Ids?: string[];\n};\n\nexport type TextractBlock = {\n  Id?: string;\n  BlockType?: string;\n  Text?: string;\n  Confidence?: number;\n  Page?: number;\n  Geometry?: TextractGeometry;\n  Relationships?: TextractRelationship[];\n};\n\nexport type TextractDocument = {\n  DocumentMetadata?: {\n    Pages?: number;\n    /**\n     * Retab fixture extension: Textract geometry is normalized and the service\n     * response does not include source page dimensions. Generated showcase\n     * fixtures can carry page dimensions here so overlays keep the source aspect.\n     */\n    PageSizes?: Array<{ Page?: number; Width?: number; Height?: number }>;\n  };\n  Blocks?: TextractBlock[];\n};\n\nexport type TextractAdapterOptions = {\n  /**\n   * Textract geometry is normalized to the page (0–1) and the response carries\n   * no pixel dimensions, so we project boxes onto a synthetic page. Defaults to\n   * a US-Letter aspect ratio.\n   */\n  pageSize?: { width: number; height: number };\n};\n\nconst DEFAULT_PAGE_SIZE = { width: 1000, height: 1294 };\n\nconst LAYOUT_BLOCK_TYPES = new Set([\n  \"LAYOUT_TITLE\",\n  \"LAYOUT_HEADER\",\n  \"LAYOUT_FOOTER\",\n  \"LAYOUT_SECTION_HEADER\",\n  \"LAYOUT_PAGE_NUMBER\",\n  \"LAYOUT_LIST\",\n  \"LAYOUT_TABLE\",\n  \"LAYOUT_FIGURE\",\n  \"LAYOUT_KEY_VALUE\",\n  \"LAYOUT_TEXT\",\n]);\n\nexport function textractToLayoutDocument(\n  input: TextractDocument,\n  options: TextractAdapterOptions = {},\n): LayoutDocument {\n  const blocks = input.Blocks ?? [];\n  const blocksById = new Map<string, TextractBlock>();\n  for (const block of blocks) {\n    if (block.Id) blocksById.set(block.Id, block);\n  }\n\n  const pageNumbers = collectPageNumbers(blocks);\n  const metadataPageSizes = collectMetadataPageSizes(input);\n  const pages: LayoutPage[] = pageNumbers.map((pageNumber) => {\n    const pageSize =\n      options.pageSize ??\n      metadataPageSizes.get(pageNumber) ??\n      DEFAULT_PAGE_SIZE;\n    return {\n      pageNumber,\n      width: pageSize.width,\n      height: pageSize.height,\n      rotation: 0,\n    };\n  });\n  const pagesByNumber = new Map(pages.map((page) => [page.pageNumber, page]));\n\n  // Reverse-index CHILD relationships so each block can find its parent.\n  const parentByChildId = new Map<string, string>();\n  for (const block of blocks) {\n    if (!block.Id) continue;\n    for (const relationship of block.Relationships ?? []) {\n      if (relationship.Type !== \"CHILD\") continue;\n      for (const childId of relationship.Ids ?? []) {\n        parentByChildId.set(childId, block.Id);\n      }\n    }\n  }\n\n  const items: LayoutItem[] = [];\n  const textParts: string[] = [];\n  let cursor = 0;\n\n  for (const block of blocks) {\n    const level = textractLevel(block.BlockType);\n    if (!level || !block.Id) continue;\n\n    const page = pagesByNumber.get(block.Page ?? 1);\n    if (!page) continue;\n\n    const quad = textractQuad(block.Geometry, page);\n    const normalizedQuad = normalizeLayoutQuad(quad, page);\n    if (!normalizedQuad) continue;\n\n    const text = textractBlockText(block, blocksById);\n    const span =\n      text.length > 0\n        ? { start: cursor, end: cursor + text.length }\n        : undefined;\n    if (text.length > 0) {\n      textParts.push(text);\n      cursor += text.length + 1;\n    }\n\n    items.push({\n      id: `textract:${block.Id}`,\n      pageNumber: page.pageNumber,\n      level,\n      kind: textractKind(block.BlockType, text),\n      text,\n      confidence: normalizeConfidence(block.Confidence),\n      quad: normalizedQuad,\n      rect: quadToRect(normalizedQuad),\n      span,\n      parentId: textractParentId(block.Id, parentByChildId),\n    });\n  }\n\n  return { text: textParts.join(\"\\n\"), pages, items };\n}\n\nfunction collectPageNumbers(blocks: TextractBlock[]): number[] {\n  const numbers = new Set<number>();\n  for (const block of blocks) {\n    numbers.add(block.Page ?? 1);\n  }\n  if (numbers.size === 0) numbers.add(1);\n  return [...numbers].sort((a, b) => a - b);\n}\n\nfunction collectMetadataPageSizes(\n  input: TextractDocument,\n): Map<number, { width: number; height: number }> {\n  const pageSizes = new Map<number, { width: number; height: number }>();\n\n  for (const pageSize of input.DocumentMetadata?.PageSizes ?? []) {\n    const pageNumber = pageSize.Page;\n    const width = finitePositiveNumber(pageSize.Width);\n    const height = finitePositiveNumber(pageSize.Height);\n    if (pageNumber == null || !width || !height) continue;\n    pageSizes.set(pageNumber, { width, height });\n  }\n\n  return pageSizes;\n}\n\nfunction textractLevel(blockType: string | undefined): LayoutLevel | undefined {\n  if (blockType === \"LINE\") return \"line\";\n  if (blockType === \"WORD\") return \"word\";\n  if (blockType && LAYOUT_BLOCK_TYPES.has(blockType)) return \"block\";\n  return undefined;\n}\n\nfunction textractParentId(\n  blockId: string,\n  parentByChildId: Map<string, string>,\n): string | undefined {\n  const parentId = parentByChildId.get(blockId);\n  return parentId ? `textract:${parentId}` : undefined;\n}\n\nfunction textractBlockText(\n  block: TextractBlock,\n  blocksById: Map<string, TextractBlock>,\n): string {\n  if (typeof block.Text === \"string\" && block.Text.length > 0) {\n    return block.Text;\n  }\n\n  // LAYOUT_* blocks carry no Text — assemble it from their child lines.\n  const childTexts: string[] = [];\n  for (const relationship of block.Relationships ?? []) {\n    if (relationship.Type !== \"CHILD\") continue;\n    for (const childId of relationship.Ids ?? []) {\n      const child = blocksById.get(childId);\n      if (child?.Text) childTexts.push(child.Text);\n    }\n  }\n  return childTexts.join(\" \");\n}\n\nfunction textractQuad(\n  geometry: TextractGeometry | undefined,\n  page: LayoutPage,\n): LayoutQuad | undefined {\n  const polygon = geometry?.Polygon;\n  if (polygon && polygon.length >= 4) {\n    return polygon.slice(0, 4).map((point) => ({\n      x: (point.X ?? 0) * page.width,\n      y: (point.Y ?? 0) * page.height,\n    })) as LayoutQuad;\n  }\n\n  const box = geometry?.BoundingBox;\n  if (box) {\n    const left = (box.Left ?? 0) * page.width;\n    const top = (box.Top ?? 0) * page.height;\n    const width = (box.Width ?? 0) * page.width;\n    const height = (box.Height ?? 0) * page.height;\n    return [\n      { x: left, y: top },\n      { x: left + width, y: top },\n      { x: left + width, y: top + height },\n      { x: left, y: top + height },\n    ];\n  }\n\n  return undefined;\n}\n\nfunction textractKind(blockType: string | undefined, text: string): LayoutKind {\n  switch (blockType) {\n    case \"LAYOUT_TITLE\":\n      return \"title\";\n    case \"LAYOUT_HEADER\":\n      return \"header\";\n    case \"LAYOUT_SECTION_HEADER\":\n      return \"heading\";\n    case \"LAYOUT_FOOTER\":\n      return \"footer\";\n    case \"LAYOUT_PAGE_NUMBER\":\n      return \"pageNumber\";\n    case \"LAYOUT_LIST\":\n      return \"list\";\n    case \"LAYOUT_TABLE\":\n      return \"table\";\n    case \"LAYOUT_FIGURE\":\n      return \"figure\";\n    case \"LAYOUT_TEXT\":\n    case \"LAYOUT_KEY_VALUE\":\n      return \"paragraph\";\n    default:\n      return /^\\d+$/.test(text.trim()) ? \"pageNumber\" : \"other\";\n  }\n}\n\nfunction normalizeConfidence(confidence: number | undefined) {\n  if (confidence == null || !Number.isFinite(confidence)) return undefined;\n  // Textract reports confidence on a 0–100 scale.\n  return Math.min(1, Math.max(0, confidence / 100));\n}\n\nfunction finitePositiveNumber(value: number | undefined) {\n  if (value == null || !Number.isFinite(value) || value <= 0) return undefined;\n  return value;\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-textract.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-azure.ts",
      "content": "import { normalizeLayoutQuad, quadToRect } from \"./layout-blocks-geometry\";\nimport type {\n  LayoutDocument,\n  LayoutItem,\n  LayoutKind,\n  LayoutLevel,\n  LayoutPage,\n  LayoutQuad,\n  LayoutTextSpan,\n} from \"./layout-blocks-types\";\n\ntype AzureSpan = {\n  offset?: number;\n  length?: number;\n};\n\ntype AzureWord = {\n  content?: string;\n  polygon?: number[];\n  confidence?: number;\n  span?: AzureSpan;\n};\n\ntype AzureLine = {\n  content?: string;\n  polygon?: number[];\n  spans?: AzureSpan[];\n};\n\ntype AzurePage = {\n  pageNumber?: number;\n  angle?: number;\n  width?: number;\n  height?: number;\n  unit?: string;\n  words?: AzureWord[];\n  lines?: AzureLine[];\n};\n\ntype AzureBoundingRegion = {\n  pageNumber?: number;\n  polygon?: number[];\n};\n\ntype AzureParagraph = {\n  role?: string;\n  content?: string;\n  boundingRegions?: AzureBoundingRegion[];\n  spans?: AzureSpan[];\n};\n\nexport type AzureAnalyzeResult = {\n  apiVersion?: string;\n  modelId?: string;\n  content?: string;\n  pages?: AzurePage[];\n  paragraphs?: AzureParagraph[];\n};\n\nexport type AzureDocument =\n  | AzureAnalyzeResult\n  | { analyzeResult?: AzureAnalyzeResult };\n\nexport function azureToLayoutDocument(input: AzureDocument): LayoutDocument {\n  const result = resolveAnalyzeResult(input);\n  const text = result.content ?? \"\";\n\n  const pages: LayoutPage[] = [];\n  const pageScaleByNumber = new Map<number, number>();\n  (result.pages ?? []).forEach((page, index) => {\n    const pageNumber = page.pageNumber ?? index + 1;\n    const scale = azureUnitScale(page.unit);\n    const width = finitePositiveNumber((page.width ?? 0) * scale);\n    const height = finitePositiveNumber((page.height ?? 0) * scale);\n    if (!width || !height) return;\n    pageScaleByNumber.set(pageNumber, scale);\n    pages.push({\n      pageNumber,\n      width,\n      height,\n      rotation: normalizeAngle(page.angle),\n    });\n  });\n  const pagesByNumber = new Map(pages.map((page) => [page.pageNumber, page]));\n\n  const blocks: LayoutItem[] = [];\n  (result.paragraphs ?? []).forEach((paragraph, index) => {\n    const region = paragraph.boundingRegions?.[0];\n    const pageNumber = region?.pageNumber;\n    const page = pageNumber != null ? pagesByNumber.get(pageNumber) : undefined;\n    if (!page) return;\n    const scale = pageScaleByNumber.get(page.pageNumber) ?? 1;\n    const item = buildItem({\n      id: `azure:p${page.pageNumber}:block:${index}`,\n      level: \"block\",\n      kind: azureRoleToKind(paragraph.role),\n      text: paragraph.content ?? \"\",\n      page,\n      polygon: region?.polygon,\n      scale,\n      span: azureSpan(paragraph.spans),\n    });\n    if (item) blocks.push(item);\n  });\n\n  const lines: LayoutItem[] = [];\n  const words: LayoutItem[] = [];\n  (result.pages ?? []).forEach((page, pageIndex) => {\n    const pageNumber = page.pageNumber ?? pageIndex + 1;\n    const layoutPage = pagesByNumber.get(pageNumber);\n    if (!layoutPage) return;\n    const scale = pageScaleByNumber.get(pageNumber) ?? 1;\n\n    (page.lines ?? []).forEach((line, index) => {\n      const item = buildItem({\n        id: `azure:p${pageNumber}:line:${index}`,\n        level: \"line\",\n        kind: azureTextKind(line.content ?? \"\"),\n        text: line.content ?? \"\",\n        page: layoutPage,\n        polygon: line.polygon,\n        scale,\n        span: azureSpan(line.spans),\n      });\n      if (item) lines.push(item);\n    });\n    (page.words ?? []).forEach((word, index) => {\n      const item = buildItem({\n        id: `azure:p${pageNumber}:word:${index}`,\n        level: \"word\",\n        kind: azureTextKind(word.content ?? \"\"),\n        text: word.content ?? \"\",\n        page: layoutPage,\n        polygon: word.polygon,\n        scale,\n        span: word.span ? azureSpan([word.span]) : undefined,\n        confidence: word.confidence,\n      });\n      if (item) words.push(item);\n    });\n  });\n\n  // Derive hierarchy from text-span containment: word → line → block.\n  const linkedWords = words.map((word) => ({\n    ...word,\n    parentId: findSmallestContainingSpan(word, lines)?.id,\n  }));\n  const linkedLines = lines.map((line) => ({\n    ...line,\n    parentId: findSmallestContainingSpan(line, blocks)?.id,\n  }));\n\n  return { text, pages, items: [...blocks, ...linkedLines, ...linkedWords] };\n}\n\nfunction buildItem({\n  id,\n  level,\n  kind,\n  text,\n  page,\n  polygon,\n  scale,\n  span,\n  confidence,\n}: {\n  id: string;\n  level: LayoutLevel;\n  kind: LayoutKind;\n  text: string;\n  page: LayoutPage;\n  polygon: number[] | undefined;\n  scale: number;\n  span?: LayoutTextSpan;\n  confidence?: number;\n}): LayoutItem | undefined {\n  const quad = azurePolygonToQuad(polygon, scale);\n  const normalizedQuad = normalizeLayoutQuad(quad, page);\n  if (!normalizedQuad) return undefined;\n\n  return {\n    id,\n    pageNumber: page.pageNumber,\n    level,\n    kind,\n    text,\n    confidence: normalizeConfidence(confidence),\n    quad: normalizedQuad,\n    rect: quadToRect(normalizedQuad),\n    span,\n  };\n}\n\nfunction resolveAnalyzeResult(input: AzureDocument): AzureAnalyzeResult {\n  if (input && \"analyzeResult\" in input && input.analyzeResult) {\n    return input.analyzeResult;\n  }\n  return input as AzureAnalyzeResult;\n}\n\nfunction azurePolygonToQuad(\n  polygon: number[] | undefined,\n  scale: number,\n): LayoutQuad | undefined {\n  if (!polygon || polygon.length < 8) return undefined;\n  return [\n    { x: polygon[0] * scale, y: polygon[1] * scale },\n    { x: polygon[2] * scale, y: polygon[3] * scale },\n    { x: polygon[4] * scale, y: polygon[5] * scale },\n    { x: polygon[6] * scale, y: polygon[7] * scale },\n  ];\n}\n\nfunction azureSpan(spans: AzureSpan[] | undefined): LayoutTextSpan | undefined {\n  if (!spans || spans.length === 0) return undefined;\n  let start = Number.POSITIVE_INFINITY;\n  let end = 0;\n  for (const span of spans) {\n    const offset = span.offset ?? 0;\n    const length = span.length ?? 0;\n    start = Math.min(start, offset);\n    end = Math.max(end, offset + length);\n  }\n  if (!Number.isFinite(start) || end < start) return undefined;\n  return { start, end };\n}\n\nfunction findSmallestContainingSpan(\n  item: LayoutItem,\n  candidates: LayoutItem[],\n): LayoutItem | undefined {\n  if (!item.span) return undefined;\n  let parent: LayoutItem | undefined;\n  let parentLength = Number.POSITIVE_INFINITY;\n\n  for (const candidate of candidates) {\n    if (!candidate.span) continue;\n    if (candidate.pageNumber !== item.pageNumber) continue;\n    if (\n      candidate.span.start > item.span.start ||\n      candidate.span.end < item.span.end\n    ) {\n      continue;\n    }\n    const candidateLength = candidate.span.end - candidate.span.start;\n    if (candidateLength < parentLength) {\n      parent = candidate;\n      parentLength = candidateLength;\n    }\n  }\n\n  return parent;\n}\n\nfunction azureUnitScale(unit: string | undefined): number {\n  switch (unit) {\n    case \"inch\":\n      return 96;\n    case \"centimeter\":\n      return 96 / 2.54;\n    case \"pixel\":\n    default:\n      return 1;\n  }\n}\n\nfunction normalizeAngle(angle: number | undefined): 0 | 90 | 180 | 270 {\n  if (angle == null || !Number.isFinite(angle)) return 0;\n  const normalized = (((Math.round(angle / 90) * 90) % 360) + 360) % 360;\n  return normalized as 0 | 90 | 180 | 270;\n}\n\nfunction azureRoleToKind(role: string | undefined): LayoutKind {\n  switch (role) {\n    case \"title\":\n      return \"title\";\n    case \"sectionHeading\":\n      return \"heading\";\n    case \"pageHeader\":\n      return \"header\";\n    case \"pageFooter\":\n      return \"footer\";\n    case \"pageNumber\":\n      return \"pageNumber\";\n    case \"footnote\":\n      return \"other\";\n    default:\n      return \"paragraph\";\n  }\n}\n\nfunction azureTextKind(text: string): LayoutKind {\n  return /^\\d+$/.test(text.trim()) ? \"pageNumber\" : \"other\";\n}\n\nfunction normalizeConfidence(confidence: number | undefined) {\n  if (confidence == null || !Number.isFinite(confidence)) return undefined;\n  return Math.min(1, Math.max(0, confidence));\n}\n\nfunction finitePositiveNumber(value: number | undefined) {\n  if (value == null || !Number.isFinite(value) || value <= 0) return undefined;\n  return value;\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-azure.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-pdf.ts",
      "content": "import type { LayoutDocument, LayoutPage } from \"./layout-blocks-types\";\n\ntype PdfImagePage = {\n  data: Uint8Array;\n  width: number;\n  height: number;\n};\n\nconst PDF_TEXT_ENCODER = new TextEncoder();\nconst PDF_JPEG_QUALITY = 0.92;\n\n/**\n * Render a normalized layout document into a PDF that the viewer can display.\n * Pages with a backdrop image (e.g. Google Document AI) draw that image; pages\n * without one (e.g. AWS Textract / Azure Document Intelligence, which return no\n * page bitmap) fall back to a blank white page sized to the page dimensions so\n * the OCR overlays still have a surface to anchor to.\n */\nexport async function layoutDocumentToPdfBlob(\n  document: LayoutDocument,\n  pageImages?: Map<number, string>,\n): Promise<Blob> {\n  if (!document.pages.length) {\n    throw new Error(\"OCR output has no pages to render.\");\n  }\n\n  const imagePages = await Promise.all(\n    document.pages.map((page) =>\n      layoutPageToPdfImage(page, pageImages?.get(page.pageNumber)),\n    ),\n  );\n\n  return new Blob([writePdf(imagePages)], { type: \"application/pdf\" });\n}\n\nasync function layoutPageToPdfImage(\n  page: LayoutPage,\n  dataUrl: string | undefined,\n): Promise<PdfImagePage> {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = Math.max(1, Math.round(page.width));\n  canvas.height = Math.max(1, Math.round(page.height));\n\n  const context = canvas.getContext(\"2d\");\n  if (!context) throw new Error(\"Canvas 2D context is unavailable.\");\n\n  context.fillStyle = \"#fff\";\n  context.fillRect(0, 0, canvas.width, canvas.height);\n\n  if (dataUrl) {\n    const image = await loadImage(dataUrl);\n    context.drawImage(image, 0, 0, canvas.width, canvas.height);\n  }\n\n  const jpegDataUrl = canvas.toDataURL(\"image/jpeg\", PDF_JPEG_QUALITY);\n  return {\n    data: dataUrlToBytes(jpegDataUrl),\n    width: canvas.width,\n    height: canvas.height,\n  };\n}\n\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n  return new Promise((resolve, reject) => {\n    const image = new Image();\n    image.onload = () => resolve(image);\n    image.onerror = () => reject(new Error(\"Failed to load OCR page image.\"));\n    image.src = src;\n  });\n}\n\nfunction writePdf(pages: PdfImagePage[]) {\n  const chunks: Uint8Array[] = [];\n  const offsets: number[] = [0];\n  let position = 0;\n  let objectNumber = 1;\n\n  function write(value: string | Uint8Array) {\n    const chunk =\n      typeof value === \"string\" ? PDF_TEXT_ENCODER.encode(value) : value;\n    chunks.push(chunk);\n    position += chunk.length;\n  }\n\n  function beginObject() {\n    offsets[objectNumber] = position;\n    write(`${objectNumber} 0 obj\\n`);\n    return objectNumber++;\n  }\n\n  function endObject() {\n    write(\"endobj\\n\");\n  }\n\n  write(\"%PDF-1.4\\n%\\xE2\\xE3\\xCF\\xD3\\n\");\n\n  const catalogObject = beginObject();\n  write(\"<< /Type /Catalog /Pages 2 0 R >>\\n\");\n  endObject();\n\n  const pagesObject = beginObject();\n  const firstPageObject = 3;\n  const pageObjectNumbers = pages.map(\n    (_, index) => firstPageObject + index * 3,\n  );\n  write(\n    `<< /Type /Pages /Count ${pages.length} /Kids [${pageObjectNumbers\n      .map((pageObject) => `${pageObject} 0 R`)\n      .join(\" \")}] >>\\n`,\n  );\n  endObject();\n\n  for (const [index, page] of pages.entries()) {\n    const pageObject = beginObject();\n    const imageObject = pageObject + 1;\n    const contentObject = pageObject + 2;\n    write(\n      `<< /Type /Page /Parent ${pagesObject} 0 R /MediaBox [0 0 ${page.width} ${page.height}] /Resources << /XObject << /Im${index + 1} ${imageObject} 0 R >> >> /Contents ${contentObject} 0 R >>\\n`,\n    );\n    endObject();\n\n    beginObject();\n    write(\n      `<< /Type /XObject /Subtype /Image /Width ${page.width} /Height ${page.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${page.data.length} >>\\nstream\\n`,\n    );\n    write(page.data);\n    write(\"\\nendstream\\n\");\n    endObject();\n\n    const content = `q\\n${page.width} 0 0 ${page.height} 0 0 cm\\n/Im${index + 1} Do\\nQ\\n`;\n    beginObject();\n    write(`<< /Length ${PDF_TEXT_ENCODER.encode(content).length} >>\\nstream\\n`);\n    write(content);\n    write(\"endstream\\n\");\n    endObject();\n  }\n\n  const xrefOffset = position;\n  write(`xref\\n0 ${objectNumber}\\n`);\n  write(\"0000000000 65535 f \\n\");\n  for (let index = 1; index < objectNumber; index += 1) {\n    write(`${String(offsets[index]).padStart(10, \"0\")} 00000 n \\n`);\n  }\n  write(\n    `trailer\\n<< /Size ${objectNumber} /Root ${catalogObject} 0 R >>\\nstartxref\\n${xrefOffset}\\n%%EOF\\n`,\n  );\n\n  const bytes = new Uint8Array(position);\n  let offset = 0;\n  for (const chunk of chunks) {\n    bytes.set(chunk, offset);\n    offset += chunk.length;\n  }\n  return bytes;\n}\n\nfunction dataUrlToBytes(dataUrl: string) {\n  const base64 = dataUrl.split(\",\")[1];\n  if (!base64) throw new Error(\"Invalid image data URL.\");\n  const binary = atob(base64);\n  const bytes = new Uint8Array(binary.length);\n  for (let index = 0; index < binary.length; index += 1) {\n    bytes[index] = binary.charCodeAt(index);\n  }\n  return bytes;\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-pdf.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-geometry.ts",
      "content": "import type {\n  LayoutItem,\n  LayoutPage,\n  LayoutPoint,\n  LayoutQuad,\n  LayoutRect,\n} from \"./layout-blocks-types\";\n\nexport function clampLayoutValue(value: number, min: number, max: number) {\n  if (!Number.isFinite(value)) return min;\n  return Math.min(max, Math.max(min, value));\n}\n\nexport function normalizeLayoutRect(\n  rect: LayoutRect | undefined,\n  page: LayoutPage,\n): LayoutRect | null {\n  if (!rect) return null;\n  if (\n    !Number.isFinite(rect.left) ||\n    !Number.isFinite(rect.top) ||\n    !Number.isFinite(rect.width) ||\n    !Number.isFinite(rect.height) ||\n    rect.width <= 0 ||\n    rect.height <= 0 ||\n    page.width <= 0 ||\n    page.height <= 0\n  ) {\n    return null;\n  }\n\n  const left = clampLayoutValue(rect.left, 0, page.width);\n  const top = clampLayoutValue(rect.top, 0, page.height);\n  const right = clampLayoutValue(rect.left + rect.width, 0, page.width);\n  const bottom = clampLayoutValue(rect.top + rect.height, 0, page.height);\n  const width = right - left;\n  const height = bottom - top;\n\n  return width > 0 && height > 0 ? { left, top, width, height } : null;\n}\n\nexport function quadToRect(quad: LayoutQuad): LayoutRect {\n  const xValues = quad.map((point) => point.x);\n  const yValues = quad.map((point) => point.y);\n  const left = Math.min(...xValues);\n  const top = Math.min(...yValues);\n  const right = Math.max(...xValues);\n  const bottom = Math.max(...yValues);\n\n  return {\n    left,\n    top,\n    width: right - left,\n    height: bottom - top,\n  };\n}\n\nexport function rectToQuad(rect: LayoutRect): LayoutQuad {\n  return [\n    { x: rect.left, y: rect.top },\n    { x: rect.left + rect.width, y: rect.top },\n    { x: rect.left + rect.width, y: rect.top + rect.height },\n    { x: rect.left, y: rect.top + rect.height },\n  ];\n}\n\nexport function normalizeLayoutQuad(\n  quad: LayoutQuad | undefined,\n  page: LayoutPage,\n): LayoutQuad | null {\n  if (!quad || page.width <= 0 || page.height <= 0) return null;\n  const normalizedQuad = quad.map((point) => ({\n    x: clampLayoutValue(point.x, 0, page.width),\n    y: clampLayoutValue(point.y, 0, page.height),\n  })) as LayoutQuad;\n  const rect = quadToRect(normalizedQuad);\n\n  return rect.width > 0 && rect.height > 0 ? normalizedQuad : null;\n}\n\nexport function normalizeRotation(rotation: number): 0 | 90 | 180 | 270 {\n  const normalized = ((rotation % 360) + 360) % 360;\n  if (normalized < 45 || normalized >= 315) return 0;\n  if (normalized < 135) return 90;\n  if (normalized < 225) return 180;\n  return 270;\n}\n\nexport function getRotatedPageSize(\n  page: LayoutPage,\n  rotation: number = page.rotation,\n) {\n  const normalizedRotation = normalizeRotation(rotation);\n  return normalizedRotation === 90 || normalizedRotation === 270\n    ? { width: page.height, height: page.width }\n    : { width: page.width, height: page.height };\n}\n\nexport function rotatePoint(\n  point: LayoutPoint,\n  page: LayoutPage,\n  rotation: number = page.rotation,\n): LayoutPoint {\n  switch (normalizeRotation(rotation)) {\n    case 90:\n      return { x: page.height - point.y, y: point.x };\n    case 180:\n      return { x: page.width - point.x, y: page.height - point.y };\n    case 270:\n      return { x: point.y, y: page.width - point.x };\n    case 0:\n    default:\n      return point;\n  }\n}\n\nexport function rotateQuad(\n  quad: LayoutQuad,\n  page: LayoutPage,\n  rotation: number = page.rotation,\n): LayoutQuad {\n  return quad.map((point) => rotatePoint(point, page, rotation)) as LayoutQuad;\n}\n\nexport function itemToQuad(\n  item: LayoutItem,\n  page: LayoutPage,\n): LayoutQuad | null {\n  const quad = normalizeLayoutQuad(item.quad, page);\n  if (quad) return quad;\n  const rect = normalizeLayoutRect(item.rect, page);\n  return rect ? rectToQuad(rect) : null;\n}\n\nexport function itemToRect(\n  item: LayoutItem,\n  page: LayoutPage,\n): LayoutRect | null {\n  const quad = itemToQuad(item, page);\n  return quad ? quadToRect(quad) : normalizeLayoutRect(item.rect, page);\n}\n\nexport function toPercentRect(\n  rect: LayoutRect,\n  page: LayoutPage,\n  rotation: number = page.rotation,\n): LayoutRect {\n  const size = getRotatedPageSize(page, rotation);\n  return {\n    left: (rect.left / size.width) * 100,\n    top: (rect.top / size.height) * 100,\n    width: (rect.width / size.width) * 100,\n    height: (rect.height / size.height) * 100,\n  };\n}\n\nexport function toSvgPoints(\n  quad: LayoutQuad,\n  page: LayoutPage,\n  rotation: number = page.rotation,\n): string {\n  const rotatedQuad = rotateQuad(quad, page, rotation);\n  const size = getRotatedPageSize(page, rotation);\n\n  return rotatedQuad\n    .map(\n      (point) =>\n        `${(point.x / size.width) * 100},${(point.y / size.height) * 100}`,\n    )\n    .join(\" \");\n}\n\nexport function getScrollTarget(item: LayoutItem, page: LayoutPage) {\n  const rect = itemToRect(item, page);\n\n  return {\n    pageNumber: item.pageNumber,\n    left: rect ? (rect.left / page.width) * 100 : 0,\n    top: rect ? (rect.top / page.height) * 100 : 0,\n    width: rect ? (rect.width / page.width) * 100 : 0,\n    height: rect ? (rect.height / page.height) * 100 : 0,\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-geometry.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-index.ts",
      "content": "import type {\n  LayoutItem,\n  LayoutLevel,\n  LayoutPage,\n} from \"./layout-blocks-types\";\n\nexport type LayoutItemIndex = {\n  itemsById: Map<string, LayoutItem>;\n  itemsByPage: Map<number, LayoutItem[]>;\n  itemsByLevel: Map<LayoutLevel, LayoutItem[]>;\n  pagesByNumber: Map<number, LayoutPage>;\n};\n\nexport function createLayoutItemIndex({\n  items,\n  pages,\n}: {\n  items: LayoutItem[];\n  pages: LayoutPage[];\n}): LayoutItemIndex {\n  const itemsById = new Map<string, LayoutItem>();\n  const itemsByPage = new Map<number, LayoutItem[]>();\n  const itemsByLevel = new Map<LayoutLevel, LayoutItem[]>();\n  const pagesByNumber = new Map<number, LayoutPage>();\n\n  for (const page of pages) {\n    pagesByNumber.set(page.pageNumber, page);\n  }\n\n  for (const item of items) {\n    itemsById.set(item.id, item);\n    appendMapValue(itemsByPage, item.pageNumber, item);\n    appendMapValue(itemsByLevel, item.level, item);\n  }\n\n  return {\n    itemsById,\n    itemsByPage,\n    itemsByLevel,\n    pagesByNumber,\n  };\n}\n\nfunction appendMapValue<Key, Value>(\n  map: Map<Key, Value[]>,\n  key: Key,\n  value: Value,\n) {\n  const values = map.get(key);\n  if (values) {\n    values.push(value);\n    return;\n  }\n  map.set(key, [value]);\n}\n\nexport function getLayoutItemsForPage({\n  index,\n  pageNumber,\n  levels,\n}: {\n  index: LayoutItemIndex;\n  pageNumber: number;\n  levels?: readonly LayoutLevel[];\n}) {\n  const pageItems = index.itemsByPage.get(pageNumber) ?? [];\n  if (!levels?.length) return pageItems;\n  const levelSet = new Set(levels);\n  return pageItems.filter((item) => levelSet.has(item.level));\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-index.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-model.ts",
      "content": "import {\n  missingEvidenceAnchor,\n  resolvedEvidenceAnchor,\n  type EvidenceItem,\n} from \"./document-evidence\";\nimport { getScrollTarget } from \"./layout-blocks-geometry\";\nimport {\n  createLayoutItemIndex as createLayoutItemIndexForItems,\n  type LayoutItemIndex,\n} from \"./layout-blocks-index\";\nimport type {\n  LayoutDocument,\n  LayoutItem,\n  LayoutLevel,\n} from \"./layout-blocks-types\";\n\nexport type LayoutEvidencePayload = {\n  item: LayoutItem;\n  level: LayoutLevel;\n  kind: string;\n  text: string;\n  confidence?: number;\n  pageNumber: number;\n};\n\nexport type LayoutEvidenceItem = EvidenceItem<LayoutEvidencePayload>;\n\nexport type LayoutBlocksFilter = {\n  levels: readonly LayoutLevel[];\n  lowConfidenceOnly?: boolean;\n  threshold: number;\n};\n\nexport type LayoutBlocksViewerModel = {\n  evidenceItems: LayoutEvidenceItem[];\n  index: LayoutItemIndex;\n  visibleItems: LayoutItem[];\n};\n\nexport function createLayoutBlocksViewerModel({\n  document,\n  levels,\n  lowConfidenceOnly = false,\n  threshold,\n}: {\n  document: LayoutDocument;\n  levels: readonly LayoutLevel[];\n  lowConfidenceOnly?: boolean;\n  threshold: number;\n}): LayoutBlocksViewerModel {\n  const index = createLayoutItemIndex(document);\n  const visibleItems = filterLayoutItems(document.items, {\n    levels,\n    lowConfidenceOnly,\n    threshold,\n  });\n\n  return {\n    ...layoutItemsToEvidenceModel(visibleItems, index),\n    index,\n    visibleItems,\n  };\n}\n\nexport function createLayoutItemIndex(\n  document: LayoutDocument,\n): LayoutItemIndex {\n  return createLayoutItemIndexForItems({\n    items: document.items,\n    pages: document.pages,\n  });\n}\n\nexport function filterLayoutItems(\n  items: readonly LayoutItem[],\n  { levels, lowConfidenceOnly = false, threshold }: LayoutBlocksFilter,\n): LayoutItem[] {\n  const levelSet = new Set(levels);\n  return items.filter((item) => {\n    if (!levelSet.has(item.level)) return false;\n    if (!lowConfidenceOnly) return true;\n    return item.confidence != null && item.confidence < threshold;\n  });\n}\n\nexport function layoutItemsToEvidenceModel(\n  items: readonly LayoutItem[],\n  index: LayoutItemIndex,\n) {\n  const evidenceItems = items.map((item) =>\n    layoutItemToEvidenceItem(item, index),\n  );\n  return {\n    evidenceItems,\n  };\n}\n\nexport function layoutItemToEvidenceItem(\n  item: LayoutItem,\n  index: LayoutItemIndex,\n): LayoutEvidenceItem {\n  const page = index.pagesByNumber.get(item.pageNumber);\n  const target = page ? getScrollTarget(item, page) : null;\n\n  return {\n    id: item.id,\n    anchor: target\n      ? resolvedEvidenceAnchor({\n          kind: \"pdf-area\",\n          pageNumber: target.pageNumber,\n          left: target.left,\n          top: target.top,\n          width: target.width,\n          height: target.height,\n        })\n      : missingEvidenceAnchor(),\n    payload: {\n      item,\n      level: item.level,\n      kind: item.kind,\n      text: item.text,\n      confidence: item.confidence,\n      pageNumber: item.pageNumber,\n    },\n  };\n}\n\nexport function layoutLevelLabel(level: LayoutLevel) {\n  if (level === \"block\") return \"Block\";\n  if (level === \"paragraph\") return \"Paragraph\";\n  if (level === \"line\") return \"Line\";\n  return \"Word\";\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-model.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-segmented-document-model.ts",
      "content": "import { buildColorMap, segmentDisplayLabel } from \"@/lib/segments\";\n\nimport { getScrollTarget } from \"./layout-blocks-geometry\";\nimport type { LayoutDocument, LayoutItem } from \"./layout-blocks-types\";\nimport {\n  createSegmentedDocumentModel,\n  type DocumentSegment,\n  type SegmentAnchor,\n  type SegmentedDocumentModel,\n} from \"./segmented-document-model\";\n\nexport function createOcrSegmentedDocumentModel({\n  document,\n  items,\n}: {\n  document: LayoutDocument;\n  items: readonly LayoutItem[];\n}): SegmentedDocumentModel {\n  const colors = buildColorMap(items.map(layoutItemSegmentLabel));\n  const pageByNumber = new Map(\n    document.pages.map((page) => [page.pageNumber, page]),\n  );\n  const segments: DocumentSegment[] = [];\n  const anchors: SegmentAnchor[] = [];\n\n  items.forEach((item, index) => {\n    const label = layoutItemSegmentLabel(item);\n    const segment: DocumentSegment = {\n      id: layoutItemSegmentId(item),\n      label,\n      pages: [],\n      color:\n        colors.get(label) ??\n        colors.get(segmentDisplayLabel(label)) ??\n        \"var(--color-muted-foreground)\",\n      confidence: item.confidence ?? null,\n      index,\n      sourceId: item.id,\n    };\n    const page = pageByNumber.get(item.pageNumber);\n    if (page) {\n      const target = getScrollTarget(item, page);\n      segment.pages = [target.pageNumber];\n      anchors.push({\n        id: `${segment.id}:anchor`,\n        segmentId: segment.id,\n        pageNumber: target.pageNumber,\n        bounds: {\n          x: target.left / 100,\n          y: target.top / 100,\n          width: target.width / 100,\n          height: target.height / 100,\n        },\n      });\n    }\n    segments.push(segment);\n  });\n\n  return createSegmentedDocumentModel({\n    anchors,\n    pages: document.pages.map((page) => ({\n      pageNumber: page.pageNumber,\n      width: page.width,\n      height: page.height,\n    })),\n    segments,\n  });\n}\n\nfunction layoutItemSegmentLabel(item: LayoutItem): string {\n  return segmentDisplayLabel(item.text || item.kind || item.level);\n}\n\nfunction layoutItemSegmentId(item: LayoutItem): string {\n  return `layout:${item.id}`;\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-segmented-document-model.ts"
    },
    {
      "path": "registry/new-york-v4/ui/layout-overlay-layer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { itemToQuad, toSvgPoints } from \"./layout-blocks-geometry\";\nimport type {\n  LayoutItem,\n  LayoutLevel,\n  LayoutPage,\n} from \"./layout-blocks-types\";\n\nconst OVERLAY_BLUE = \"rgb(37 99 235)\";\nconst OVERLAY_ACTIVE_PURPLE = \"rgb(147 51 234)\";\nconst OVERLAY_ACTIVE_PURPLE_FILL = \"rgb(147 51 234 / 0.12)\";\nconst OVERLAY_STROKE_WIDTH = 1.6;\nconst OVERLAY_ACTIVE_STROKE_WIDTH = 2.2;\n\nexport function LayoutOverlayLayer({\n  activeItemId,\n  className,\n  interactive = false,\n  items,\n  onItemClick,\n  onItemPointerEnter,\n  onItemPointerLeave,\n  page,\n  rotation = page.rotation,\n  selectedItemId,\n  visibleLevels,\n}: {\n  activeItemId?: string | null;\n  className?: string;\n  interactive?: boolean;\n  items: LayoutItem[];\n  onItemClick?: (item: LayoutItem) => void;\n  onItemPointerEnter?: (item: LayoutItem) => void;\n  onItemPointerLeave?: () => void;\n  page: LayoutPage;\n  rotation?: number;\n  selectedItemId?: string | null;\n  visibleLevels?: readonly LayoutLevel[];\n}) {\n  const visibleLevelSet = React.useMemo(\n    () => (visibleLevels?.length ? new Set(visibleLevels) : null),\n    [visibleLevels],\n  );\n  const renderedItems = React.useMemo(\n    () =>\n      visibleLevelSet\n        ? items.filter((item) => visibleLevelSet.has(item.level))\n        : items,\n    [items, visibleLevelSet],\n  );\n\n  return (\n    <svg\n      aria-hidden={!interactive}\n      className={cn(\n        \"absolute inset-0 z-10 size-full\",\n        interactive ? \"pointer-events-auto\" : \"pointer-events-none\",\n        className,\n      )}\n      preserveAspectRatio=\"none\"\n      viewBox=\"0 0 100 100\"\n    >\n      {renderedItems.map((item) => {\n        const quad = itemToQuad(item, page);\n        if (!quad) return null;\n\n        const isActive = item.id === activeItemId;\n        const isSelected = item.id === selectedItemId;\n        const isEmphasized = isActive || isSelected;\n\n        return (\n          <polygon\n            key={item.id}\n            data-item-id={item.id}\n            points={toSvgPoints(quad, page, rotation)}\n            vectorEffect=\"non-scaling-stroke\"\n            role={interactive ? \"button\" : undefined}\n            tabIndex={interactive ? 0 : undefined}\n            aria-label={\n              interactive\n                ? `${item.level} ${item.text.trim() || \"layout item\"}`\n                : undefined\n            }\n            className={cn(\n              \"transition-[fill,stroke,stroke-width]\",\n              interactive && \"cursor-pointer outline-none\",\n              interactive && \"focus-visible:outline-none\",\n            )}\n            style={{\n              fill: isEmphasized ? OVERLAY_ACTIVE_PURPLE_FILL : \"transparent\",\n              stroke: isEmphasized ? OVERLAY_ACTIVE_PURPLE : OVERLAY_BLUE,\n            }}\n            strokeWidth={\n              isEmphasized ? OVERLAY_ACTIVE_STROKE_WIDTH : OVERLAY_STROKE_WIDTH\n            }\n            onClick={() => onItemClick?.(item)}\n            onKeyDown={(event) => {\n              if (!interactive) return;\n              if (event.key === \"Enter\" || event.key === \" \") {\n                event.preventDefault();\n                onItemClick?.(item);\n              }\n            }}\n            onPointerEnter={() => onItemPointerEnter?.(item)}\n            onPointerLeave={onItemPointerLeave}\n          />\n        );\n      })}\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-overlay-layer.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/layout-blocks-panel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { InteractiveItemList } from \"./interactive-item-list\";\nimport {\n  layoutLevelLabel,\n  type LayoutEvidenceItem,\n} from \"./layout-blocks-model\";\n\nconst ROW_ESTIMATE_SIZE = 84;\n\nexport function LayoutBlocksPanel({\n  activeItemId,\n  className,\n  emptyLabel = \"No layout items found.\",\n  items,\n  onActiveItemIdChange,\n  onNavigateItem,\n  onSelectedItemIdChange,\n  renderItemContent,\n  selectedItemId,\n}: {\n  activeItemId?: string | null;\n  className?: string;\n  emptyLabel?: string;\n  items: LayoutEvidenceItem[];\n  onActiveItemIdChange?: (itemId: string | null) => void;\n  onNavigateItem?: (\n    item: LayoutEvidenceItem,\n    options?: ScrollToOptions,\n  ) => void;\n  onSelectedItemIdChange?: (itemId: string | null) => void;\n  renderItemContent?: (item: LayoutEvidenceItem) => React.ReactNode;\n  selectedItemId?: string | null;\n}) {\n  return (\n    <div\n      data-slot=\"layout-blocks-panel\"\n      className={cn(\"bg-background flex min-h-0 flex-col\", className)}\n    >\n      <InteractiveItemList\n        aria-label=\"OCR blocks\"\n        activeItemId={activeItemId}\n        emptyLabel={emptyLabel}\n        estimateSize={ROW_ESTIMATE_SIZE}\n        items={items}\n        onActivateItem={(item) => {\n          onSelectedItemIdChange?.(item.id);\n          onNavigateItem?.(item, { behavior: \"smooth\" });\n        }}\n        onClearPreview={() => onActiveItemIdChange?.(null)}\n        onClearSelection={() => onSelectedItemIdChange?.(null)}\n        onPreviewItem={(item) => {\n          onActiveItemIdChange?.(item.id);\n          onNavigateItem?.(item, { behavior: \"auto\" });\n        }}\n        onVisibleItemChange={(item) => {\n          onActiveItemIdChange?.(item.id);\n          onNavigateItem?.(item, { behavior: \"auto\" });\n        }}\n        renderItem={(item, state) => (\n          <LayoutItemRow\n            item={item}\n            isActive={state.isActive}\n            isSelected={state.isSelected}\n            renderItemContent={renderItemContent}\n          />\n        )}\n        selectedItemId={selectedItemId}\n      />\n    </div>\n  );\n}\n\nfunction LayoutItemRow({\n  isActive,\n  isSelected,\n  item,\n  renderItemContent,\n}: {\n  isActive: boolean;\n  isSelected: boolean;\n  item: LayoutEvidenceItem;\n  renderItemContent?: (item: LayoutEvidenceItem) => React.ReactNode;\n}) {\n  const { confidence, pageNumber, text: rawText } = item.payload;\n  const confidenceLabel =\n    confidence == null ? \"Unknown\" : `${Math.round(confidence * 100)}%`;\n  const text = rawText.replace(/\\s+/g, \" \").trim();\n\n  return (\n    <span\n      className={cn(\n        \"bg-background hover:bg-muted/40 focus-visible:ring-ring block w-full rounded-lg border p-3 text-left transition-[background-color,border-color,box-shadow] outline-none focus-visible:ring-2\",\n        isActive && \"border-primary/50 bg-primary/5\",\n        isSelected && \"border-primary bg-primary/8 shadow-sm\",\n      )}\n    >\n      <div className=\"flex min-w-0 items-center gap-2\">\n        <span className=\"bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 text-[11px] font-medium\">\n          {layoutLevelLabel(item.payload.level)}\n        </span>\n        <span\n          className={cn(\n            \"text-[11px] font-medium\",\n            confidence != null && confidence < 0.9\n              ? \"text-destructive\"\n              : \"text-muted-foreground\",\n          )}\n        >\n          {confidenceLabel}\n        </span>\n        <span className=\"text-muted-foreground ml-auto text-[11px]\">\n          p. {pageNumber}\n        </span>\n      </div>\n      <div className=\"text-foreground mt-2 text-sm leading-5\">\n        {renderItemContent ? (\n          renderItemContent(item)\n        ) : (\n          <span className=\"line-clamp-3\">{text || \"Empty text\"}</span>\n        )}\n      </div>\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/layout-blocks-panel.tsx"
    }
  ],
  "type": "registry:ui"
}