{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "split-viewer-block",
  "title": "Split Viewer",
  "description": "Named subdocuments over a PDF: a legend header and a vertical page-ribbon sidebar, from the shared segment primitives.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/file-viewer",
    "@retab/pdf-viewer",
    "@retab/segment-legend",
    "@retab/segment-page-rail",
    "@retab/segmented-document",
    "@retab/segments"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/split-viewer-block.tsx",
      "content": "\"use client\";\n\nimport {\n  FileViewerContent,\n  FileViewerHeader,\n  FileViewerTitle,\n  FileViewerLegend,\n  FileViewer,\n  FileViewerProvider,\n  FileViewerSidebarTrigger,\n  FileViewerInset,\n  FileViewerControls,\n} from \"@/components/ui/file-viewer\";\nimport { PdfViewerPages, PdfViewerProvider } from \"@/components/ui/pdf-viewer\";\nimport type { SplitView } from \"@/components/viewers/lib/split-types\";\nimport {\n  SplitViewerDocument,\n  SplitViewerLegend,\n  SplitViewerProvider,\n  SplitViewerSidebar,\n  useSplitViewerDocumentControls,\n} from \"@/components/viewers/split/split-viewer\";\n\nconst PDF_URL = \"/samples/an-image-is-worth-16x16-words.pdf\";\n\n// A split result: named subdocuments, each owning a 1-indexed page range.\nconst SPLIT_RESULT: SplitView = {\n  output: [\n    { name: \"Title, Abstract & Introduction\", pages: [1] },\n    { name: \"Related Work\", pages: [2] },\n    { name: \"Method\", pages: [3] },\n    { name: \"Experiments\", pages: [4, 5, 6, 7, 8] },\n    { name: \"Conclusion & References\", pages: [9, 10, 11, 12] },\n    { name: \"Appendix\", pages: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22] },\n  ],\n};\n\n/**\n * Split viewer block — the file + sidebar + legend system over a split result.\n * `SplitViewer` owns the legend and page rail; the document prop reads page\n * and scroll controls from the split provider.\n */\nexport function SplitViewerBlock() {\n  const source = {\n    kind: \"url\" as const,\n    url: PDF_URL,\n    fileName: \"an-image-is-worth-16x16-words.pdf\",\n  };\n\n  return (\n    <div className=\"bg-background flex h-full min-h-[680px] flex-col\">\n      <SplitViewerProvider result={SPLIT_RESULT}>\n        <FileViewerProvider source={source} defaultSidebarOpen>\n          <FileViewer className=\"bg-background\">\n            <PdfViewerProvider>\n              <FileViewerHeader>\n                  <FileViewerSidebarTrigger className=\"-ms-1\" />\n                  <FileViewerTitle />\n                  <FileViewerControls />\n              </FileViewerHeader>\n              <FileViewerContent>\n                <SplitViewerSidebar />\n                <FileViewerInset>\n                  <FileViewerLegend>\n                    <SplitViewerLegend className=\"px-3 py-2\" />\n                  </FileViewerLegend>\n                  <SplitViewerDocument document={<SplitViewerPdfDocument />} />\n                </FileViewerInset>\n              </FileViewerContent>\n            </PdfViewerProvider>\n          </FileViewer>\n        </FileViewerProvider>\n      </SplitViewerProvider>\n    </div>\n  );\n}\n\nfunction SplitViewerPdfDocument() {\n  const controls = useSplitViewerDocumentControls();\n\n  return (\n    <PdfViewerPages\n      ref={controls.setDocumentHandle}\n      bare\n      onVisiblePageChange={controls.onCurrentPageChange}\n      onScrollProgressChange={controls.onScrollProgressChange}\n      className=\"h-full\"\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/blocks/split-viewer-block.tsx"
    },
    {
      "path": "components/viewers/split/split-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { type ReactNode } from \"react\";\nimport { Loader2, Scissors } from \"lucide-react\";\n\nimport { segmentsPageCount, toSegments } from \"@/lib/segments\";\nimport { cn } from \"@/lib/utils\";\nimport type { ViewerSource } from \"@/lib/viewer-source\";\nimport {\n  FileViewerContent,\n  FileViewerHeader,\n  FileViewerTitle,\n  FileViewerLegend,\n  FileViewer,\n  FileViewerProvider,\n  FileViewerSidebar,\n  FileViewerSidebarContent,\n  FileViewerSidebarTrigger,\n  FileViewerInset,\n  FileViewerControls,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport { SegmentLegend } from \"@/components/ui/segment-legend\";\nimport { SegmentPageRail } from \"@/components/ui/segment-page-rail\";\nimport {\n  createSegmentedDocumentModel,\n  type DocumentSegment,\n  type SegmentedDocumentModel,\n} from \"@/components/ui/segmented-document-model\";\nimport {\n  SegmentedDocumentProvider,\n  useSegmentedDocumentViewport,\n} from \"@/components/ui/segmented-document-provider\";\nimport {\n  type SegmentDocumentHandle,\n  type SegmentViewportController,\n} from \"@/components/ui/use-segment-viewport-controller\";\nimport { ViewerHeader } from \"@/components/ui/viewer\";\nimport { type SplitView } from \"@/components/viewers/lib/split-types\";\n\nexport interface SplitDocumentHandlers {\n  onCurrentPageChange: (page: number) => void;\n  onScrollProgressChange: (progress: number) => void;\n  setDocumentHandle: (handle: SegmentDocumentHandle | null) => void;\n}\n\nexport interface SplitViewerProps {\n  result: SplitView | null;\n  source: ViewerSource;\n  isProcessing?: boolean;\n  document?: ReactNode;\n}\n\nexport type SplitViewerSidebarProps = React.ComponentProps<\n  typeof FileViewerSidebar\n>;\n\nexport function SplitViewer({\n  result,\n  source,\n  isProcessing = false,\n  document,\n}: SplitViewerProps) {\n  return (\n    <SplitViewerProvider result={result} isProcessing={isProcessing}>\n      <FileViewerProvider\n        source={source}\n        headerMode=\"outlets\"\n        defaultSidebarOpen\n      >\n        <FileViewer className=\"bg-background\">\n          <SplitViewerFileHeader />\n          <FileViewerContent>\n            <SplitViewerSidebar />\n            <FileViewerInset>\n              <FileViewerLegend>\n                <SplitViewerLegend className=\"px-3 py-2\" />\n              </FileViewerLegend>\n              <SplitViewerDocument document={document} />\n            </FileViewerInset>\n          </FileViewerContent>\n        </FileViewer>\n      </FileViewerProvider>\n    </SplitViewerProvider>\n  );\n}\n\nfunction SplitViewerFileHeader() {\n  return (\n    <FileViewerHeader>\n        <FileViewerSidebarTrigger />\n        <FileViewerTitle />\n        <FileViewerControls />\n    </FileViewerHeader>\n  );\n}\n\ntype SplitViewerContextValue = {\n  model: SplitViewerModel;\n  viewport: SegmentViewportController;\n};\n\nexport type SplitViewerModel = {\n  hasOutput: boolean;\n  isProcessing: boolean;\n  pageCount: number;\n  segments: DocumentSegment[];\n};\n\ntype SplitViewerHeaderState = {\n  hasOutput: boolean;\n  isProcessing: boolean;\n  pageCount: number;\n  segments: DocumentSegment[];\n};\n\ntype SplitViewerSidebarState = {\n  hasOutput: boolean;\n  pageCount: number;\n};\n\ntype SplitViewerPageRailState = {\n  hasOutput: boolean;\n  pageCount: number;\n  segments: DocumentSegment[];\n  viewport: SegmentViewportController;\n};\n\ntype SplitViewerLegendState = {\n  hasOutput: boolean;\n  segments: DocumentSegment[];\n  viewport: SegmentViewportController;\n};\n\ntype SplitViewerDocumentState = {\n  hasOutput: boolean;\n  isProcessing: boolean;\n};\n\nconst SplitViewerContext = React.createContext<SplitViewerContextValue | null>(\n  null,\n);\n\nfunction useSplitViewerContext(): SplitViewerContextValue {\n  const context = React.useContext(SplitViewerContext);\n  if (!context) {\n    throw new Error(\"useSplitViewer must be used within SplitViewerProvider.\");\n  }\n  return context;\n}\n\nfunction useSplitViewerHeader(): SplitViewerHeaderState {\n  return useSplitViewerContext().model;\n}\n\nfunction useSplitViewerSidebar(): SplitViewerSidebarState {\n  const { hasOutput, pageCount } = useSplitViewerContext().model;\n  return { hasOutput, pageCount };\n}\n\nfunction useSplitViewerPageRail(): SplitViewerPageRailState {\n  const { model, viewport } = useSplitViewerContext();\n  return {\n    hasOutput: model.hasOutput,\n    pageCount: model.pageCount,\n    segments: model.segments,\n    viewport,\n  };\n}\n\nfunction useSplitViewerLegend(): SplitViewerLegendState {\n  const { model, viewport } = useSplitViewerContext();\n  return { hasOutput: model.hasOutput, segments: model.segments, viewport };\n}\n\nfunction useSplitViewerDocument(): SplitViewerDocumentState {\n  const { hasOutput, isProcessing } = useSplitViewerContext().model;\n  return { hasOutput, isProcessing };\n}\n\nexport function useSplitViewerDocumentControls(): SplitDocumentHandlers {\n  return useSplitViewerContext().viewport.documentHandlers;\n}\n\nexport function createSplitViewerModel({\n  result,\n  isProcessing,\n}: {\n  result: SplitView | null;\n  isProcessing: boolean;\n}): SplitViewerModel {\n  const segments = toSegments(result?.output ?? []) satisfies DocumentSegment[];\n\n  return {\n    hasOutput: Boolean(result && result.output.length > 0),\n    isProcessing,\n    pageCount: segmentsPageCount(segments),\n    segments,\n  };\n}\n\nfunction createSplitSegmentedDocumentModel(\n  model: Pick<SplitViewerModel, \"pageCount\" | \"segments\">,\n): SegmentedDocumentModel {\n  return createSegmentedDocumentModel({\n    pageCount: model.pageCount,\n    segments: model.segments,\n  });\n}\n\nexport function SplitViewerProvider({\n  result,\n  isProcessing = false,\n  children,\n}: {\n  result: SplitView | null;\n  isProcessing?: boolean;\n  children: React.ReactNode;\n}) {\n  const model = React.useMemo(\n    () => createSplitViewerModel({ result, isProcessing }),\n    [isProcessing, result],\n  );\n  const segmentedDocumentModel = React.useMemo(\n    () => createSplitSegmentedDocumentModel(model),\n    [model],\n  );\n\n  return (\n    <SegmentedDocumentProvider model={segmentedDocumentModel}>\n      <SplitViewerContextProvider model={model}>\n        {children}\n      </SplitViewerContextProvider>\n    </SegmentedDocumentProvider>\n  );\n}\n\nfunction SplitViewerContextProvider({\n  children,\n  model,\n}: {\n  children: React.ReactNode;\n  model: SplitViewerModel;\n}) {\n  const viewport = useSegmentedDocumentViewport();\n\n  const value = React.useMemo<SplitViewerContextValue>(\n    () => ({\n      model,\n      viewport,\n    }),\n    [model, viewport],\n  );\n\n  return (\n    <SplitViewerContext.Provider value={value}>\n      {children}\n    </SplitViewerContext.Provider>\n  );\n}\n\nexport function SplitViewerHeader() {\n  const { hasOutput, isProcessing, pageCount, segments } =\n    useSplitViewerHeader();\n  const title = hasOutput\n    ? `${segments.length} segment${segments.length === 1 ? \"\" : \"s\"}`\n    : isProcessing\n      ? \"Splitting\"\n      : \"Split viewer\";\n\n  return (\n    <ViewerHeader className=\"flex flex-col\">\n      <div className=\"flex min-h-10 items-center justify-between gap-3 px-3 py-2\">\n        <div className=\"flex items-center gap-2 text-sm font-medium\">\n          {hasOutput && pageCount > 0 ? (\n            <FileViewerSidebarTrigger className=\"-ml-1\" />\n          ) : null}\n          <Scissors className=\"text-muted-foreground size-4\" />\n          <span>{title}</span>\n        </div>\n        {hasOutput ? (\n          <div className=\"text-muted-foreground text-xs\">\n            {pageCount} page{pageCount === 1 ? \"\" : \"s\"}\n          </div>\n        ) : null}\n      </div>\n    </ViewerHeader>\n  );\n}\n\nexport function SplitViewerSidebar({\n  children,\n  className,\n  width = \"4rem\",\n  \"aria-label\": ariaLabel = \"Split pages\",\n  ...props\n}: SplitViewerSidebarProps) {\n  const { hasOutput, pageCount } = useSplitViewerSidebar();\n  if (!hasOutput || pageCount <= 0) return null;\n\n  return (\n    <FileViewerSidebar\n      aria-label={ariaLabel}\n      width={width}\n      className={cn(\"border-r\", className)}\n      {...props}\n    >\n      <FileViewerSidebarContent className=\"p-0\">\n        {children ?? <SplitViewerPageRail />}\n      </FileViewerSidebarContent>\n    </FileViewerSidebar>\n  );\n}\n\nexport function SplitViewerPageRail() {\n  const { hasOutput, pageCount, segments, viewport } = useSplitViewerPageRail();\n  if (!hasOutput || pageCount <= 0) return null;\n\n  return (\n    <SegmentPageRail\n      segments={segments}\n      pageCount={pageCount}\n      currentPage={viewport.model.currentPage}\n      scrollProgress={viewport.model.scrollProgress}\n      interaction={viewport.interaction}\n      railApi={viewport.rail}\n      onSelectPage={viewport.navigation.scrollToPage}\n      showTicks\n    />\n  );\n}\n\nexport function SplitViewerLegend({ className }: { className?: string }) {\n  const { hasOutput, segments, viewport } = useSplitViewerLegend();\n  if (!hasOutput) return null;\n\n  return (\n    <SegmentLegend\n      segments={segments}\n      currentPage={viewport.model.currentPage}\n      interaction={viewport.interaction}\n      onSelect={viewport.navigation.scrollToSegmentStart}\n      columns={4}\n      variant=\"plain\"\n      showUnusedToggle\n      className={className}\n    />\n  );\n}\n\nexport function SplitViewerDocument({ document }: { document?: ReactNode }) {\n  const { hasOutput, isProcessing } = useSplitViewerDocument();\n\n  if (!hasOutput) {\n    return <SplitViewerEmptyState isProcessing={isProcessing} />;\n  }\n\n  return document ? (\n    <FileViewerViewport>{document}</FileViewerViewport>\n  ) : (\n    <div className=\"flex h-full flex-1 items-center justify-center\">\n      <span className=\"text-muted-foreground text-sm\">\n        No document available\n      </span>\n    </div>\n  );\n}\n\nexport function SplitViewerEmptyState({\n  isProcessing,\n}: {\n  isProcessing: boolean;\n}) {\n  return (\n    <div className=\"bg-background text-muted-foreground flex h-full flex-1 flex-col items-center justify-center gap-4 px-8\">\n      {isProcessing ? (\n        <>\n          <Loader2 className=\"text-warning-foreground h-12 w-12 animate-spin\" />\n          <p className=\"text-muted-foreground text-center text-base\">\n            Splitting...\n          </p>\n        </>\n      ) : (\n        <>\n          <Scissors className=\"text-muted-foreground h-16 w-16\" />\n          <p className=\"text-muted-foreground text-center text-base\">\n            Run split to see output\n          </p>\n          <p className=\"text-muted-foreground max-w-sm text-center text-sm\">\n            Upload a document, define subdocuments, then click Run Split\n          </p>\n        </>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/split/split-viewer.tsx"
    },
    {
      "path": "components/viewers/lib/split-types.ts",
      "content": "// Split result shapes (the structural subset the viewer needs), mirrored from\n// the Retab dashboard's SplitView.\n\nimport type { PartitionChunk } from \"@/components/viewers/lib/partition-types\";\n\nexport interface SplitResult {\n  name: string;\n  /** 1-indexed pages assigned to this subdocument. */\n  pages: number[];\n  /** Frontend-only overlay carried through by the split viewer. */\n  partitions?: PartitionChunk[];\n}\n\nexport interface SplitSubdocumentLikelihood {\n  /** Consensus confidence for the subdocument label. */\n  name?: number | null;\n  /** Consensus confidence per assigned page, aligned with the output pages. */\n  pages?: number[] | null;\n}\n\nexport interface SplitViewConsensus {\n  choices?: SplitResult[][];\n  likelihoods?: SplitSubdocumentLikelihood[] | null;\n}\n\nexport interface SplitView {\n  output: SplitResult[];\n  consensus?: SplitViewConsensus | null;\n  usage?: { credits: number } | null;\n}\n\nexport function asSplitView(\n  payload:\n    | { output?: unknown; consensus?: unknown; usage?: unknown }\n    | null\n    | undefined,\n): SplitView | null {\n  if (!payload || !Array.isArray(payload.output)) return null;\n  return {\n    output: payload.output as SplitResult[],\n    consensus: (payload.consensus ?? null) as SplitViewConsensus | null,\n    usage: (payload.usage ?? null) as SplitView[\"usage\"],\n  };\n}\n\nexport function getSplitVotes(\n  splitView: SplitView | null | undefined,\n  splitIndex: number,\n): SplitResult[] {\n  const choices = splitView?.consensus?.choices;\n  if (!choices?.length) return [];\n  return choices.flatMap((choice) => {\n    const split = choice[splitIndex];\n    return split ? [split] : [];\n  });\n}\n",
      "type": "registry:lib",
      "target": "@components/viewers/lib/split-types.ts"
    },
    {
      "path": "components/viewers/lib/partition-types.ts",
      "content": "// Partition result shapes, mirrored from the Retab API / dashboard.\n\nexport interface PartitionChunk {\n  key: string;\n  /** 1-indexed pages assigned to this chunk. */\n  pages: number[];\n}\n\nexport interface PartitionChunkLikelihood {\n  key: number | null;\n  pages: number[];\n}\n\nexport interface PartitionConsensus {\n  choices: PartitionChunk[][];\n  likelihoods: PartitionChunkLikelihood[] | null;\n}\n\nexport interface PartitionUsage {\n  credits: number;\n}\n\nexport interface PartitionResult {\n  output: PartitionChunk[];\n  consensus: PartitionConsensus;\n  usage: PartitionUsage | null;\n}\n",
      "type": "registry:lib",
      "target": "@components/viewers/lib/partition-types.ts"
    }
  ],
  "categories": [
    "primitives"
  ],
  "type": "registry:block"
}