{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "txt-splitting-viewer-block",
  "title": "Text Splitting",
  "description": "One plain-text batch file separated into several documents — each carved-out document rendered in the viewer, with a left sidebar listing the source file first, then every document the splitter emitted, its type, line count, and the boundary cue that detected it. Selecting one opens that document.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/file-viewer",
    "@retab/text-viewer",
    "@retab/segments"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/txt-splitting-viewer-block.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { FileText, WrapText } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  FileViewer,\n  FileViewerContent,\n  FileViewerControls,\n  FileViewerHeader,\n  FileViewerInset,\n  FileViewerProvider,\n  FileViewerSidebar,\n  FileViewerSidebarContent,\n  FileViewerSidebarTrigger,\n  FileViewerTitle,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport { TextViewer } from \"@/components/ui/text-viewer\";\nimport { SEGMENT_PALETTE } from \"@/lib/segments\";\nimport txtOutputs from \"@/components/viewers/sample-data/txt-splitting.json\";\n\nconst SOURCE_ID = \"source\";\nconst SOURCE_NAME = \"intake-batch.txt\";\nconst TEXT_URL = \"/samples/intake-batch.txt\";\n\nconst SOURCE_SOURCE = {\n  kind: \"url\" as const,\n  url: TEXT_URL,\n  fileName: SOURCE_NAME,\n};\n\ntype TxtId = keyof typeof txtOutputs;\n\n/**\n * One document the text splitter carved out of the batch file. `kind` is the\n * document type it was classified as; `detection` is the boundary cue that\n * separated it from its neighbours. The document text itself lives in the\n * sample-data JSON, keyed by `id`.\n */\ntype TxtOutput = {\n  id: TxtId;\n  fileName: string;\n  kind: string;\n  detection: string;\n};\n\nconst TXT_OUTPUTS: TxtOutput[] = [\n  {\n    id: \"cover-letter\",\n    fileName: \"cover-letter.txt\",\n    kind: \"Cover letter\",\n    detection: \"Salutation and signature block\",\n  },\n  {\n    id: \"invoice\",\n    fileName: \"invoice.txt\",\n    kind: \"Invoice\",\n    detection: \"Invoice number and line-item table\",\n  },\n  {\n    id: \"delivery-receipt\",\n    fileName: \"delivery-receipt.txt\",\n    kind: \"Delivery receipt\",\n    detection: \"Received-by header and gate pass\",\n  },\n  {\n    id: \"purchase-order\",\n    fileName: \"purchase-order.txt\",\n    kind: \"Purchase order\",\n    detection: \"PO number and not-to-exceed amount\",\n  },\n];\n\n// Stable {kind:\"text\"} source per document so selecting one swaps the viewer\n// without re-creating the resource on every render.\nconst TXT_SOURCE_BY_ID = new Map(\n  TXT_OUTPUTS.map((output) => [\n    output.id,\n    {\n      kind: \"text\" as const,\n      text: txtOutputs[output.id],\n      fileName: output.fileName,\n    },\n  ]),\n);\n\nfunction lineCount(id: TxtId) {\n  return txtOutputs[id].split(\"\\n\").length;\n}\n\nfunction outputColor(index: number) {\n  return SEGMENT_PALETTE[index % SEGMENT_PALETTE.length];\n}\n\n/**\n * Text splitting block — one plain-text batch file separated into several\n * documents. A left sidebar lists the source file first, then every document\n * the splitter carved out. Selecting an entry swaps the viewer to that\n * document: the full batch file for the source, each carved-out text otherwise.\n */\nexport function TxtSplittingViewerBlock() {\n  const [activeId, setActiveId] = React.useState<string>(SOURCE_ID);\n  const isSource = activeId === SOURCE_ID;\n  const activeSource = isSource\n    ? SOURCE_SOURCE\n    : TXT_SOURCE_BY_ID.get(activeId as TxtId)!;\n\n  return (\n    <FileViewerProvider source={activeSource} defaultSidebarOpen>\n      <FileViewer className=\"bg-background h-full min-h-[680px]\">\n        <FileViewerHeader>\n          <FileViewerSidebarTrigger className=\"-ms-1\" />\n          <FileViewerTitle />\n          <FileViewerControls />\n        </FileViewerHeader>\n        <FileViewerContent>\n          <FileViewerSidebar\n            aria-label=\"Split output\"\n            side=\"left\"\n            width=\"300px\"\n          >\n            <FileViewerSidebarContent className=\"gap-0 p-0\">\n              <div className=\"border-b px-4 py-3\">\n                <p className=\"text-sm font-medium\">Split output</p>\n                <p className=\"text-muted-foreground text-xs\">\n                  Source file &rarr; {TXT_OUTPUTS.length} documents\n                </p>\n              </div>\n              <ul className=\"min-h-0 flex-1 overflow-y-auto\">\n                <li>\n                  <SourceRow\n                    active={isSource}\n                    onSelect={() => setActiveId(SOURCE_ID)}\n                  />\n                </li>\n                <li>\n                  <p className=\"text-muted-foreground/70 border-y px-4 pt-3 pb-1 text-[11px] font-medium tracking-wide uppercase\">\n                    Split documents\n                  </p>\n                </li>\n                {TXT_OUTPUTS.map((output, index) => (\n                  <li key={output.id}>\n                    <TxtOutputRow\n                      output={output}\n                      color={outputColor(index)}\n                      lines={lineCount(output.id)}\n                      active={output.id === activeId}\n                      onSelect={() => setActiveId(output.id)}\n                    />\n                  </li>\n                ))}\n              </ul>\n            </FileViewerSidebarContent>\n          </FileViewerSidebar>\n          <FileViewerInset>\n            <FileViewerViewport>\n              <TextViewer\n                source={activeSource}\n                bare\n                controls={false}\n                mode=\"text\"\n                className=\"h-full\"\n              />\n            </FileViewerViewport>\n          </FileViewerInset>\n        </FileViewerContent>\n      </FileViewer>\n    </FileViewerProvider>\n  );\n}\n\n/** The source batch file — the first sidebar entry. */\nfunction SourceRow({\n  active,\n  onSelect,\n}: {\n  active: boolean;\n  onSelect: () => void;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onSelect}\n      aria-pressed={active}\n      className={cn(\n        \"focus-visible:ring-ring relative flex w-full items-center gap-2.5 px-4 py-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none\",\n        active ? \"bg-muted/60\" : \"hover:bg-muted/40\",\n      )}\n    >\n      <span\n        aria-hidden\n        className={cn(\n          \"bg-foreground absolute inset-y-0 left-0 w-0.5 transition-opacity\",\n          active ? \"opacity-100\" : \"opacity-0\",\n        )}\n      />\n      <FileText className=\"text-muted-foreground size-4 shrink-0\" />\n      <span className=\"min-w-0 flex-1\">\n        <span className=\"text-foreground block truncate font-mono text-sm\">\n          {SOURCE_NAME}\n        </span>\n        <span className=\"text-muted-foreground block truncate text-xs\">\n          Source file\n        </span>\n      </span>\n    </button>\n  );\n}\n\n/** One carved-out document in the sidebar. */\nfunction TxtOutputRow({\n  output,\n  color,\n  lines,\n  active,\n  onSelect,\n}: {\n  output: TxtOutput;\n  color: string;\n  lines: number;\n  active: boolean;\n  onSelect: () => void;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onSelect}\n      aria-pressed={active}\n      className={cn(\n        \"focus-visible:ring-ring relative flex w-full flex-col gap-1.5 px-4 py-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none\",\n        active ? \"bg-muted/60\" : \"hover:bg-muted/40\",\n      )}\n    >\n      <span\n        aria-hidden\n        className={cn(\n          \"absolute inset-y-0 left-0 w-0.5 transition-opacity\",\n          active ? \"opacity-100\" : \"opacity-0\",\n        )}\n        style={{ backgroundColor: color }}\n      />\n      <div className=\"flex items-center gap-2\">\n        <span\n          aria-hidden\n          className=\"size-2.5 shrink-0 rounded-full\"\n          style={{ backgroundColor: color }}\n        />\n        <span className=\"text-foreground truncate font-mono text-sm\">\n          {output.fileName}\n        </span>\n        <span className=\"text-muted-foreground ml-auto inline-flex items-center gap-1 font-mono text-[11px]\">\n          <WrapText className=\"size-3\" />\n          {lines} lines\n        </span>\n      </div>\n      <div className=\"text-muted-foreground truncate pl-[18px] text-xs\">\n        {output.kind}\n      </div>\n      <div className=\"text-muted-foreground/80 truncate pl-[18px] text-[11px]\">\n        {output.detection}\n      </div>\n    </button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/txt-splitting-viewer-block.tsx"
    },
    {
      "path": "components/viewers/sample-data/txt-splitting.json",
      "content": "{\n  \"cover-letter\": \"COVER LETTER\\n\\nAtlas Freight Solutions\\n118 Harbor Way, Oakland, CA 94607\\n\\nJune 8, 2026\\n\\nTo the Accounts Payable team at Acme Logistics,\\n\\nPlease find enclosed our invoice for the May haulage window,\\nthe matching delivery receipt, and the original purchase order\\nyour team issued in April. All three reference PO-4471.\\n\\nWe have applied the 2% early-settlement discount agreed in the\\nmaster services agreement. Payment terms remain net 30 from the\\ninvoice date. Reach out to billing@atlasfreight.example with any\\nquestions.\\n\\nKind regards,\\nDana Whitfield\\nBilling Lead, Atlas Freight Solutions\",\n  \"invoice\": \"INVOICE\\n\\nAtlas Freight Solutions            Invoice #: INV-2026-0559\\n118 Harbor Way                     Invoice date: 2026-06-08\\nOakland, CA 94607                  Terms: Net 30\\n                                   Reference: PO-4471\\n\\nBill to:\\n  Acme Logistics\\n  900 Cannery Row, Monterey, CA 93940\\n\\nLine items:\\n  Long-haul freight, Oakland to Reno       12 loads    $ 9,600.00\\n  Fuel surcharge                            12 loads    $ 1,140.00\\n  Detention time                            6 hours     $   450.00\\n  Pallet exchange fee                       220 units   $   330.00\\n\\n  Subtotal                                              $11,520.00\\n  Early-settlement discount (2%)                        $  -230.40\\n  Tax (8.5%)                                            $   959.42\\n  Total due                                             $12,249.02\",\n  \"delivery-receipt\": \"DELIVERY RECEIPT\\n\\nReceived by: Acme Logistics receiving dock 3\\nDate/time:   2026-05-29 14:12 PDT\\nDriver:      M. Okonkwo, unit 22\\nReference:   PO-4471 / INV-2026-0559\\n\\nCondition on arrival: 12 of 12 loads intact, seals unbroken.\\nTwo pallets flagged for re-wrap, noted on gate pass.\\nTemperature log within range for the full transit window.\\n\\nReceiving signature: J. Alvarez\\nGate pass: GP-88231\",\n  \"purchase-order\": \"PURCHASE ORDER\\n\\nPurchase Order: PO-4471            Issued: 2026-04-15\\nBuyer:  Acme Logistics            Approved by: R. Nguyen\\nVendor: Atlas Freight Solutions   Cost center: LOG-WEST\\n\\nScope:\\n  Contracted long-haul freight, Oakland to Reno corridor,\\n  up to 12 loads for the May 2026 window, with fuel surcharge\\n  billed at the published monthly index.\\n\\nNot-to-exceed amount: $13,000.00\\nDelivery-by: 2026-05-31\\nSpecial terms: 2% discount if settled within 10 days of invoice.\"\n}\n",
      "type": "registry:file",
      "target": "@components/viewers/sample-data/txt-splitting.json"
    }
  ],
  "categories": [
    "primitives"
  ],
  "type": "registry:block"
}