{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "text-sources-block",
  "title": "Text Sources",
  "description": "Values extracted from a log file linked to the lines they came from — hover to highlight a line range.",
  "registryDependencies": [
    "@retab/text-viewer",
    "@retab/text-source",
    "@retab/document-source",
    "@retab/segmented-document",
    "@retab/source-segmented-document",
    "@retab/source-field-list",
    "@retab/source-field-link",
    "@retab/source-indicator"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/text-sources-block.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { Source } from \"@/lib/document-source\";\nimport {\n  FileViewer,\n  FileViewerContent,\n  FileViewerHeader,\n  FileViewerTitle,\n  FileViewerProvider,\n  FileViewerSidebar,\n  FileViewerSidebarContent,\n  FileViewerInset,\n  FileViewerControls,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport { SegmentedDocumentProvider } from \"@/components/ui/segmented-document-provider\";\nimport { useSegmentedSourceFieldLink } from \"@/components/ui/source-field-link\";\nimport {\n  SourceFieldList,\n  type SourceField,\n} from \"@/components/ui/source-field-list\";\nimport { createSourcesSegmentedDocumentModel } from \"@/components/ui/source-segmented-document-model\";\nimport {\n  sourceToTextHighlight,\n  useTextSourceTarget,\n} from \"@/components/ui/text-source\";\nimport { TextViewer, type TextViewerHandle } from \"@/components/ui/text-viewer\";\nimport textSample from \"@/components/viewers/sample-data/text-sources.json\";\n\nconst TEXT_URL = \"/samples/extraction-run.log\";\nconst TEXT_SOURCE = {\n  kind: \"url\" as const,\n  url: TEXT_URL,\n  fileName: \"extraction-run.log\",\n};\n\ntype TextField = SourceField & { source: Source };\n\nconst FIELDS = (textSample as TextField[]).map((field) => ({\n  ...field,\n  hint:\n    field.source.anchor.kind === \"text_span\"\n      ? `Line ${field.source.anchor.line_start}`\n      : undefined,\n}));\nconst FIELD_BY_KEY = new Map(FIELDS.map((field) => [field.key, field]));\nconst SEGMENTED_DOCUMENT = createSourcesSegmentedDocumentModel(\n  FIELDS.map((field) => ({\n    id: field.key,\n    label: field.label,\n    source: field.source,\n  })),\n);\n\n/**\n * Text sources block — values extracted from a log file, linked to the lines\n * they came from. Hovering a field highlights its line range and scrolls to it.\n * Segmented source interaction owns field preview/selection while the text\n * source adapter owns line-range scrolling and highlighting.\n */\nexport function TextSourcesBlock() {\n  const viewerRef = React.useRef<TextViewerHandle>(null);\n\n  return (\n    <SegmentedDocumentProvider model={SEGMENTED_DOCUMENT}>\n      <TextSourcesContent viewerRef={viewerRef} />\n    </SegmentedDocumentProvider>\n  );\n}\n\nfunction TextSourcesContent({\n  viewerRef,\n}: {\n  viewerRef: React.RefObject<TextViewerHandle | null>;\n}) {\n  const target = useTextSourceTarget(viewerRef);\n  const segmentedLink = useSegmentedSourceFieldLink({\n    initialSourcePath: FIELDS[0]?.key,\n  });\n  const link = useTargetedSourceFieldLink({\n    fieldByKey: FIELD_BY_KEY,\n    link: segmentedLink,\n    target,\n  });\n  const activeSource = link.activeSourcePath\n    ? FIELD_BY_KEY.get(link.activeSourcePath)?.source\n    : undefined;\n  const highlight = sourceToTextHighlight(activeSource);\n\n  return (\n    <FileViewerProvider source={TEXT_SOURCE} defaultSidebarOpen>\n      <FileViewer\n        className=\"bg-background h-full min-h-[680px]\"\n       \n      >\n        <FileViewerHeader>\n            <FileViewerTitle />\n            <FileViewerControls />\n        </FileViewerHeader>\n        <FileViewerContent>\n          <FileViewerInset>\n            <FileViewerViewport>\n              <TextViewer\n                ref={viewerRef}\n                source={TEXT_SOURCE}\n                bare\n                className=\"h-full\"\n                controls={false}\n                highlight={highlight}\n                mode=\"text\"\n              />\n            </FileViewerViewport>\n          </FileViewerInset>\n          <FileViewerSidebar\n            aria-label=\"Source fields\"\n            side=\"right\"\n            collapsible=\"none\"\n            width=\"360px\"\n            className=\"border-l\"\n          >\n            <FileViewerSidebarContent>\n              <SourceFieldList fields={FIELDS} link={link} />\n            </FileViewerSidebarContent>\n          </FileViewerSidebar>\n        </FileViewerContent>\n      </FileViewer>\n    </FileViewerProvider>\n  );\n}\n\nfunction useTargetedSourceFieldLink({\n  fieldByKey,\n  link,\n  target,\n}: {\n  fieldByKey: ReadonlyMap<string, TextField>;\n  link: ReturnType<typeof useSegmentedSourceFieldLink>;\n  target: ReturnType<typeof useTextSourceTarget>;\n}) {\n  const scrollToField = React.useCallback(\n    (path: string, behavior: ScrollBehavior) => {\n      const source = fieldByKey.get(path)?.source;\n      if (source) target.scrollTo?.(source, { behavior });\n    },\n    [fieldByKey, target],\n  );\n  const onSourceHover = React.useCallback(\n    (path: string | null) => {\n      link.onSourceHover(path);\n      if (path) scrollToField(path, \"auto\");\n    },\n    [link, scrollToField],\n  );\n  const selectSourcePath = React.useCallback(\n    (path: string) => {\n      link.selectSourcePath?.(path);\n      scrollToField(path, \"smooth\");\n    },\n    [link, scrollToField],\n  );\n\n  return React.useMemo(\n    () => ({\n      ...link,\n      onSourceHover,\n      selectSourcePath,\n    }),\n    [link, onSourceHover, selectSourcePath],\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/text-sources-block.tsx"
    },
    {
      "path": "components/viewers/sample-data/text-sources.json",
      "content": "[\n  {\n    \"key\": \"runId\",\n    \"label\": \"Run ID\",\n    \"value\": \"run_3xK9b2QvT7\",\n    \"source\": {\n      \"content\": \"run_id=run_3xK9b2QvT7\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 2, \"line_end\": 2 }\n    }\n  },\n  {\n    \"key\": \"tenant\",\n    \"label\": \"Tenant\",\n    \"value\": \"acme-corp\",\n    \"source\": {\n      \"content\": \"tenant=acme-corp environment=prod\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 3, \"line_end\": 3 }\n    }\n  },\n  {\n    \"key\": \"environment\",\n    \"label\": \"Environment\",\n    \"value\": \"prod\",\n    \"source\": {\n      \"content\": \"tenant=acme-corp environment=prod\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 3, \"line_end\": 3 }\n    }\n  },\n  {\n    \"key\": \"fileName\",\n    \"label\": \"File name\",\n    \"value\": \"bank-statement.pdf\",\n    \"source\": {\n      \"content\": \"received file bank-statement.pdf (80.1 KB)\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 4, \"line_end\": 4 }\n    }\n  },\n  {\n    \"key\": \"fileSize\",\n    \"label\": \"File size\",\n    \"value\": \"80.1 KB\",\n    \"source\": {\n      \"content\": \"received file bank-statement.pdf (80.1 KB)\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 4, \"line_end\": 4 }\n    }\n  },\n  {\n    \"key\": \"sha256\",\n    \"label\": \"SHA-256\",\n    \"value\": \"4f1a…c0de\",\n    \"source\": {\n      \"content\": \"sha256=4f1a...c0de\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 5, \"line_end\": 5 }\n    }\n  },\n  {\n    \"key\": \"mimeType\",\n    \"label\": \"MIME type\",\n    \"value\": \"application/pdf\",\n    \"source\": {\n      \"content\": \"mime=application/pdf\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 6, \"line_end\": 6 }\n    }\n  },\n  {\n    \"key\": \"documentType\",\n    \"label\": \"Document type\",\n    \"value\": \"bank_statement\",\n    \"source\": {\n      \"content\": \"detected document_type=bank_statement\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 7, \"line_end\": 7 }\n    }\n  },\n  {\n    \"key\": \"classifierConfidence\",\n    \"label\": \"Classifier confidence\",\n    \"value\": \"0.984\",\n    \"source\": {\n      \"content\": \"confidence=0.984\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 8, \"line_end\": 8 }\n    }\n  },\n  {\n    \"key\": \"pageCount\",\n    \"label\": \"Page count\",\n    \"value\": \"4\",\n    \"source\": {\n      \"content\": \"page_count=4\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 9, \"line_end\": 9 }\n    }\n  },\n  {\n    \"key\": \"ocrDpi\",\n    \"label\": \"OCR resolution\",\n    \"value\": \"192 dpi\",\n    \"source\": {\n      \"content\": \"rasterizing pages at 192 dpi\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 11, \"line_end\": 11 }\n    }\n  },\n  {\n    \"key\": \"model\",\n    \"label\": \"Extraction model\",\n    \"value\": \"retab-large\",\n    \"source\": {\n      \"content\": \"model=retab-large n_consensus=3\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 16, \"line_end\": 16 }\n    }\n  },\n  {\n    \"key\": \"consensusRuns\",\n    \"label\": \"Consensus runs\",\n    \"value\": \"3\",\n    \"source\": {\n      \"content\": \"model=retab-large n_consensus=3\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 16, \"line_end\": 16 }\n    }\n  },\n  {\n    \"key\": \"schema\",\n    \"label\": \"Schema\",\n    \"value\": \"bank_statement_v2\",\n    \"source\": {\n      \"content\": \"schema=bank_statement_v2 fields=18\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 17, \"line_end\": 17 }\n    }\n  },\n  {\n    \"key\": \"schemaFields\",\n    \"label\": \"Schema fields\",\n    \"value\": \"18\",\n    \"source\": {\n      \"content\": \"schema=bank_statement_v2 fields=18\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 17, \"line_end\": 17 }\n    }\n  },\n  {\n    \"key\": \"meanLikelihood\",\n    \"label\": \"Mean likelihood\",\n    \"value\": \"0.91\",\n    \"source\": {\n      \"content\": \"merged 3 runs, mean_likelihood=0.91\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 21, \"line_end\": 21 }\n    }\n  },\n  {\n    \"key\": \"lowAgreementField\",\n    \"label\": \"Low-agreement field\",\n    \"value\": \"transactions.7.amount\",\n    \"source\": {\n      \"content\": \"field transactions.7.amount low agreement (0.58)\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 22, \"line_end\": 22 }\n    }\n  },\n  {\n    \"key\": \"fieldsGrounded\",\n    \"label\": \"Fields grounded\",\n    \"value\": \"16 / 18\",\n    \"source\": {\n      \"content\": \"16/18 fields grounded\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 26, \"line_end\": 26 }\n    }\n  },\n  {\n    \"key\": \"reviewQueue\",\n    \"label\": \"Review queue\",\n    \"value\": \"finance-ops\",\n    \"source\": {\n      \"content\": \"assigned to queue=finance-ops\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 29, \"line_end\": 29 }\n    }\n  },\n  {\n    \"key\": \"promptTokens\",\n    \"label\": \"Prompt tokens\",\n    \"value\": \"14,820\",\n    \"source\": {\n      \"content\": \"prompt_tokens=14820 completion_tokens=2110\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 30, \"line_end\": 30 }\n    }\n  },\n  {\n    \"key\": \"completionTokens\",\n    \"label\": \"Completion tokens\",\n    \"value\": \"2,110\",\n    \"source\": {\n      \"content\": \"prompt_tokens=14820 completion_tokens=2110\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 30, \"line_end\": 30 }\n    }\n  },\n  {\n    \"key\": \"totalTokens\",\n    \"label\": \"Total tokens\",\n    \"value\": \"16,930\",\n    \"source\": {\n      \"content\": \"total_tokens=16930\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 31, \"line_end\": 31 }\n    }\n  },\n  {\n    \"key\": \"status\",\n    \"label\": \"Run status\",\n    \"value\": \"completed\",\n    \"source\": {\n      \"content\": \"run status=completed\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 32, \"line_end\": 32 }\n    }\n  },\n  {\n    \"key\": \"durationMs\",\n    \"label\": \"Duration\",\n    \"value\": \"11,842 ms\",\n    \"source\": {\n      \"content\": \"duration_ms=11842\",\n      \"anchor\": { \"kind\": \"text_span\", \"line_start\": 33, \"line_end\": 33 }\n    }\n  }\n]\n",
      "type": "registry:file",
      "target": "@components/viewers/sample-data/text-sources.json"
    }
  ],
  "categories": [
    "primitives"
  ],
  "type": "registry:block"
}