{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "partition-viewer-block",
  "title": "Partition Viewer",
  "description": "Keyed chunks over a PDF: a legend plus a horizontal page-ribbon waterfall, from the shared segment primitives.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@retab/file-viewer",
    "@retab/pdf-viewer",
    "@retab/segment-legend",
    "@retab/page-ribbon",
    "@retab/segmented-document",
    "@retab/segments"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/blocks/partition-viewer-block.tsx",
      "content": "\"use client\";\n\nimport {\n  FileViewerContent,\n  FileViewerHeader,\n  FileViewerTitle,\n  FileViewerLegend,\n  FileViewer,\n  FileViewerProvider,\n  FileViewerInset,\n  FileViewerControls,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport { PdfViewerPages, PdfViewerProvider } from \"@/components/ui/pdf-viewer\";\nimport type { PartitionResult } from \"@/components/viewers/lib/partition-types\";\nimport {\n  PartitionViewerHeaderMeta,\n  PartitionViewerLegend,\n  PartitionViewerProvider,\n  PartitionViewerRibbon,\n  usePartitionViewerDocumentControls,\n} from \"@/components/viewers/partition/partition-viewer\";\n\nconst PDF_URL = \"/samples/an-image-is-worth-16x16-words.pdf\";\n\n// A partition result: keyed chunks, each owning a set of 1-indexed pages.\nconst PARTITION_RESULT: PartitionResult = {\n  output: [\n    { key: \"abstract\", pages: [1] },\n    { key: \"introduction\", pages: [1, 2] },\n    { key: \"related_work\", pages: [2] },\n    { key: \"method\", pages: [3, 4] },\n    { key: \"experiments\", pages: [4, 5, 6, 7, 8] },\n    { key: \"conclusion\", pages: [9] },\n    { key: \"references\", pages: [9, 10, 11, 12] },\n    { key: \"appendix\", pages: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22] },\n  ],\n  consensus: { choices: [], likelihoods: null },\n  usage: null,\n};\n\n/**\n * Partition viewer block — the file + legend + waterfall ribbon over keyed\n * chunks. The provider owns the key and ribbon state; the document surface is\n * visible JSX.\n */\nexport function PartitionViewerBlock() {\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      <PartitionViewerProvider result={PARTITION_RESULT}>\n        <FileViewerProvider source={source}>\n          <FileViewer className=\"bg-background\">\n            <PdfViewerProvider>\n              <FileViewerHeader>\n                  <FileViewerTitle />\n                  <PartitionViewerHeaderMeta />\n                  <FileViewerControls />\n              </FileViewerHeader>\n              <FileViewerContent>\n                <FileViewerInset>\n                  <FileViewerLegend>\n                    <PartitionViewerLegend className=\"px-3 py-2\" />\n                  </FileViewerLegend>\n                  <PartitionViewerRibbon />\n                  <FileViewerViewport>\n                    <PartitionSourceDocument />\n                  </FileViewerViewport>\n                </FileViewerInset>\n              </FileViewerContent>\n            </PdfViewerProvider>\n          </FileViewer>\n        </FileViewerProvider>\n      </PartitionViewerProvider>\n    </div>\n  );\n}\n\nfunction PartitionSourceDocument() {\n  const controls = usePartitionViewerDocumentControls();\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/partition-viewer-block.tsx"
    },
    {
      "path": "components/viewers/partition/partition-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Key, Loader2 } from \"lucide-react\";\n\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  FileViewerInset,\n  FileViewerControls,\n  FileViewerViewport,\n} from \"@/components/ui/file-viewer\";\nimport { PageRibbon } from \"@/components/ui/page-ribbon\";\nimport { SegmentLegend } from \"@/components/ui/segment-legend\";\nimport {\n  SegmentedDocumentProvider,\n  useSegmentedDocumentViewport,\n} from \"@/components/ui/segmented-document-provider\";\nimport { type SegmentViewportController } from \"@/components/ui/use-segment-viewport-controller\";\nimport { ViewerHeader } from \"@/components/ui/viewer\";\nimport type { PartitionResult } from \"@/components/viewers/lib/partition-types\";\n\nimport {\n  createPartitionSegmentedDocumentModel,\n  createPartitionViewerModel,\n  type PartitionViewerModel,\n} from \"./partition-viewer-model\";\n\nexport type PartitionDocumentControls =\n  SegmentViewportController[\"documentHandlers\"];\n\ntype PartitionViewerContextValue = {\n  isProcessing: boolean;\n  model: PartitionViewerModel;\n  viewport: SegmentViewportController;\n};\n\ntype PartitionViewerHeaderState = {\n  currentPage: SegmentViewportController[\"model\"][\"currentPage\"];\n  interaction: SegmentViewportController[\"interaction\"];\n  legendSegments: PartitionViewerModel[\"legendSegments\"];\n  navigation: SegmentViewportController[\"navigation\"];\n  pageCount: number;\n};\n\ntype PartitionViewerRibbonState = {\n  currentPage: SegmentViewportController[\"model\"][\"currentPage\"];\n  interaction: SegmentViewportController[\"interaction\"];\n  navigation: SegmentViewportController[\"navigation\"];\n  pageCount: number;\n  rows: PartitionViewerModel[\"ribbonRows\"];\n  scrollProgress: SegmentViewportController[\"model\"][\"scrollProgress\"];\n};\n\ntype PartitionViewerDocumentState = {\n  hasOutput: boolean;\n};\n\ntype PartitionViewerEmptyStatusState = {\n  isProcessing: boolean;\n};\n\nconst PartitionViewerContext =\n  React.createContext<PartitionViewerContextValue | null>(null);\n\nexport interface PartitionViewerProviderProps {\n  result: PartitionResult | null;\n  isProcessing?: boolean;\n  children: React.ReactNode;\n}\n\nexport interface PartitionViewerProps {\n  result: PartitionResult | null;\n  source: ViewerSource;\n  isProcessing?: boolean;\n  document?: React.ReactNode;\n}\n\nfunction usePartitionViewerContext(): PartitionViewerContextValue {\n  const context = React.useContext(PartitionViewerContext);\n  if (!context) {\n    throw new Error(\n      \"usePartitionViewer must be used within PartitionViewerProvider.\",\n    );\n  }\n  return context;\n}\n\nfunction usePartitionViewerHeader(): PartitionViewerHeaderState {\n  const { model, viewport } = usePartitionViewerContext();\n\n  return {\n    currentPage: viewport.model.currentPage,\n    interaction: viewport.interaction,\n    legendSegments: model.legendSegments,\n    navigation: viewport.navigation,\n    pageCount: model.pageCount,\n  };\n}\n\nfunction usePartitionViewerRibbon(): PartitionViewerRibbonState {\n  const { model, viewport } = usePartitionViewerContext();\n\n  return {\n    currentPage: viewport.model.currentPage,\n    interaction: viewport.interaction,\n    navigation: viewport.navigation,\n    pageCount: model.pageCount,\n    rows: model.ribbonRows,\n    scrollProgress: viewport.model.scrollProgress,\n  };\n}\n\nexport function usePartitionViewerDocumentControls(): PartitionDocumentControls {\n  return usePartitionViewerContext().viewport.documentHandlers;\n}\n\nfunction usePartitionViewerDocument(): PartitionViewerDocumentState {\n  return {\n    hasOutput: usePartitionViewerContext().model.hasOutput,\n  };\n}\n\nfunction usePartitionViewerEmpty(): PartitionViewerEmptyStatusState {\n  return {\n    isProcessing: usePartitionViewerContext().isProcessing,\n  };\n}\n\nexport function PartitionViewerProvider({\n  result,\n  isProcessing = false,\n  children,\n}: PartitionViewerProviderProps) {\n  const model = React.useMemo(\n    () => createPartitionViewerModel(result),\n    [result],\n  );\n  const segmentedDocumentModel = React.useMemo(\n    () => createPartitionSegmentedDocumentModel(model),\n    [model],\n  );\n\n  return (\n    <SegmentedDocumentProvider model={segmentedDocumentModel}>\n      <PartitionViewerContextProvider isProcessing={isProcessing} model={model}>\n        {children}\n      </PartitionViewerContextProvider>\n    </SegmentedDocumentProvider>\n  );\n}\n\nfunction PartitionViewerContextProvider({\n  children,\n  isProcessing,\n  model,\n}: {\n  children: React.ReactNode;\n  isProcessing: boolean;\n  model: PartitionViewerModel;\n}) {\n  const viewport = useSegmentedDocumentViewport();\n\n  const value = React.useMemo<PartitionViewerContextValue>(\n    () => ({\n      isProcessing,\n      model,\n      viewport,\n    }),\n    [isProcessing, model, viewport],\n  );\n\n  return (\n    <PartitionViewerContext.Provider value={value}>\n      {children}\n    </PartitionViewerContext.Provider>\n  );\n}\n\nexport function PartitionViewerHeader({\n  className,\n  trailing,\n}: {\n  className?: string;\n  trailing?: React.ReactNode;\n}) {\n  const { currentPage, interaction, legendSegments, navigation } =\n    usePartitionViewerHeader();\n\n  if (legendSegments.length === 0) return null;\n\n  return (\n    <ViewerHeader className={className ?? \"bg-background space-y-2 px-3 py-2\"}>\n      <SegmentLegend\n        variant=\"plain\"\n        segments={legendSegments}\n        currentPage={currentPage}\n        interaction={interaction}\n        onSelect={navigation.scrollToSegmentStart}\n        columns={4}\n      />\n      {trailing}\n    </ViewerHeader>\n  );\n}\n\nexport function PartitionViewerHeaderMeta({\n  className,\n}: {\n  className?: string;\n}) {\n  const { currentPage, legendSegments, pageCount } = usePartitionViewerHeader();\n  const text = formatHeaderPageLabel({ currentPage, pageCount });\n\n  if (legendSegments.length === 0 || !text) return null;\n\n  return (\n    <span className={cn(\"text-muted-foreground shrink-0 text-xs\", className)}>\n      {text}\n    </span>\n  );\n}\n\nfunction formatHeaderPageLabel({\n  currentPage,\n  pageCount,\n}: {\n  currentPage: number | null;\n  pageCount: number;\n}) {\n  if (pageCount <= 0) return null;\n\n  const page = Math.min(Math.max(currentPage ?? 1, 1), pageCount);\n  return `Page ${page}`;\n}\n\nexport function PartitionViewerLegend({ className }: { className?: string }) {\n  const { currentPage, interaction, legendSegments, navigation } =\n    usePartitionViewerHeader();\n\n  if (legendSegments.length === 0) return null;\n\n  return (\n    <SegmentLegend\n      variant=\"plain\"\n      segments={legendSegments}\n      currentPage={currentPage}\n      interaction={interaction}\n      onSelect={navigation.scrollToSegmentStart}\n      columns={4}\n      className={className}\n    />\n  );\n}\n\nexport function PartitionViewerRibbon({ className }: { className?: string }) {\n  const {\n    currentPage,\n    interaction,\n    navigation,\n    pageCount,\n    rows,\n    scrollProgress,\n  } = usePartitionViewerRibbon();\n\n  if (rows.length === 0) return null;\n\n  return (\n    <div className={className ?? \"bg-background border-b px-3 py-2\"}>\n      <PageRibbon\n        orientation=\"horizontal\"\n        rows={rows}\n        pageCount={pageCount}\n        currentPage={currentPage}\n        scrollProgress={scrollProgress}\n        interaction={interaction}\n        onSelectPage={navigation.scrollToPage}\n      />\n    </div>\n  );\n}\n\nexport function PartitionViewerDocument({\n  document,\n}: {\n  document?: React.ReactNode;\n}) {\n  const { hasOutput } = usePartitionViewerDocument();\n\n  if (!hasOutput) return <PartitionViewerEmptyState />;\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 PartitionViewerEmptyState() {\n  const { isProcessing } = usePartitionViewerEmpty();\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-primary h-12 w-12 animate-spin\" />\n          <p className=\"text-muted-foreground text-center text-base\">\n            Partitioning...\n          </p>\n        </>\n      ) : (\n        <>\n          <Key className=\"text-muted-foreground h-16 w-16\" />\n          <p className=\"text-muted-foreground text-center text-base\">\n            Run partition to see output\n          </p>\n          <p className=\"text-muted-foreground max-w-sm text-center text-sm\">\n            Upload a document, set a key and instructions, then click Run\n            Partition\n          </p>\n        </>\n      )}\n    </div>\n  );\n}\n\nexport function PartitionViewer({\n  result,\n  source,\n  isProcessing = false,\n  document,\n}: PartitionViewerProps) {\n  return (\n    <PartitionViewerProvider result={result} isProcessing={isProcessing}>\n      <FileViewerProvider source={source} headerMode=\"outlets\">\n        <FileViewer className=\"bg-background\">\n          <PartitionViewerFileHeader />\n          <FileViewerContent>\n            <FileViewerInset>\n              <FileViewerLegend>\n                <PartitionViewerLegend className=\"px-3 py-2\" />\n              </FileViewerLegend>\n              <PartitionViewerRibbon />\n              <PartitionViewerDocument document={document} />\n            </FileViewerInset>\n          </FileViewerContent>\n        </FileViewer>\n      </FileViewerProvider>\n    </PartitionViewerProvider>\n  );\n}\n\nfunction PartitionViewerFileHeader() {\n  return (\n    <FileViewerHeader>\n        <FileViewerTitle />\n        <PartitionViewerHeaderMeta />\n        <FileViewerControls />\n    </FileViewerHeader>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/partition/partition-viewer.tsx"
    },
    {
      "path": "components/viewers/partition/partition-viewer-model.ts",
      "content": "import {\n  buildColorMap,\n  segmentDisplayLabel,\n  segmentsPageCount,\n} from \"@/lib/segments\";\nimport { type RibbonRow } from \"@/components/ui/page-ribbon\";\nimport {\n  createSegmentedDocumentModel,\n  type DocumentSegment,\n  type SegmentedDocumentModel,\n} from \"@/components/ui/segmented-document-model\";\nimport type {\n  PartitionChunk,\n  PartitionResult,\n} from \"@/components/viewers/lib/partition-types\";\n\nexport type PartitionRibbonRowKind = \"output\" | \"vote\";\n\nexport type PartitionRibbonRow = RibbonRow & {\n  kind: PartitionRibbonRowKind;\n  voteIndex?: number;\n};\n\nexport type PartitionViewerModel = {\n  hasOutput: boolean;\n  legendSegments: DocumentSegment[];\n  pageCount: number;\n  ribbonRows: PartitionRibbonRow[];\n  viewportSegments: DocumentSegment[];\n};\n\nexport function createPartitionViewerModel(\n  result: PartitionResult | null,\n): PartitionViewerModel {\n  if (!result) return emptyPartitionViewerModel();\n\n  const voteChoices = result.consensus.choices ?? [];\n  const colors = buildColorMap([\n    ...result.output.map((chunk) => chunk.key),\n    ...voteChoices.flat().map((chunk) => chunk.key),\n  ]);\n  const legendSegments = createPartitionLegendSegments(result.output, colors);\n  const viewportSegments = legendSegments;\n  const ribbonRows = createPartitionRibbonRows(result, colors);\n  const ribbonSegments = ribbonRows.flatMap((row) => row.segments);\n\n  return {\n    hasOutput: result.output.length > 0,\n    legendSegments,\n    pageCount: segmentsPageCount(ribbonSegments),\n    ribbonRows,\n    viewportSegments,\n  };\n}\n\nexport function createPartitionLegendSegments(\n  output: readonly PartitionChunk[],\n  colors: ReadonlyMap<string, string>,\n): DocumentSegment[] {\n  const segmentsByLabel = new Map<string, DocumentSegment>();\n\n  for (const chunk of output) {\n    const label = partitionDisplayLabel(chunk.key);\n    const existing = segmentsByLabel.get(label);\n\n    if (existing) {\n      segmentsByLabel.set(label, {\n        ...existing,\n        pages: normalizePartitionPages([...existing.pages, ...chunk.pages]),\n      });\n      continue;\n    }\n\n    segmentsByLabel.set(label, {\n      id: partitionSegmentId(label),\n      label,\n      pages: normalizePartitionPages(chunk.pages),\n      color: partitionColor(chunk.key, colors),\n      index: segmentsByLabel.size,\n    });\n  }\n\n  return [...segmentsByLabel.values()].map((segment, index) => ({\n    ...segment,\n    index,\n  }));\n}\n\nexport function createPartitionRibbonRows(\n  result: PartitionResult,\n  colors: ReadonlyMap<string, string>,\n): PartitionRibbonRow[] {\n  const voteChoices = result.consensus.choices ?? [];\n  return [\n    ...result.output.map((chunk, index) => ({\n      id: `output:${index}`,\n      kind: \"output\" as const,\n      segments: [createPartitionSegment(chunk, index, colors)],\n    })),\n    ...voteChoices.flatMap((chunks, voteIndex) =>\n      chunks.map((chunk, index) => ({\n        id: `vote:${voteIndex}:${index}`,\n        kind: \"vote\" as const,\n        voteIndex,\n        segments: [createPartitionSegment(chunk, index, colors)],\n      })),\n    ),\n  ];\n}\n\nexport function normalizePartitionPages(pages: readonly number[]): number[] {\n  return Array.from(\n    new Set((pages ?? []).filter((page) => Number.isInteger(page) && page > 0)),\n  ).sort((a, b) => a - b);\n}\n\nfunction createPartitionSegment(\n  chunk: PartitionChunk,\n  index: number,\n  colors: ReadonlyMap<string, string>,\n): DocumentSegment {\n  const label = partitionDisplayLabel(chunk.key);\n\n  return {\n    id: partitionSegmentId(label),\n    label,\n    pages: normalizePartitionPages(chunk.pages),\n    color: partitionColor(chunk.key, colors),\n    index,\n  };\n}\n\nexport function createPartitionSegmentedDocumentModel(\n  model: Pick<\n    PartitionViewerModel,\n    \"pageCount\" | \"ribbonRows\" | \"viewportSegments\"\n  >,\n): SegmentedDocumentModel {\n  return createSegmentedDocumentModel({\n    pageCount: model.pageCount,\n    rows: model.ribbonRows,\n    segments: model.viewportSegments,\n  });\n}\n\nfunction emptyPartitionViewerModel(): PartitionViewerModel {\n  return {\n    hasOutput: false,\n    legendSegments: [],\n    pageCount: 0,\n    ribbonRows: [],\n    viewportSegments: [],\n  };\n}\n\nfunction partitionColor(key: string, colors: ReadonlyMap<string, string>) {\n  return (\n    colors.get(key) ??\n    colors.get(segmentDisplayLabel(key)) ??\n    \"var(--color-muted-foreground)\"\n  );\n}\n\nfunction partitionDisplayLabel(key: string) {\n  return segmentDisplayLabel(key);\n}\n\nfunction partitionSegmentId(label: string) {\n  return `partition:${label}`;\n}\n",
      "type": "registry:component",
      "target": "@components/viewers/partition/partition-viewer-model.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": [
    "dropzone"
  ],
  "type": "registry:block"
}