{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitive-run-cards",
  "title": "Primitive Run Card Components",
  "description": "Prop-driven run cards for each Retab primitive — classify, split, partition, parse, and extract — each framing the source document with that primitive's result over the shared RunCard shell. Presentational: pass the resolved result (category, segments, markdown, or extract fields), so the same cards render a static demo or a live workflow run's per-block result.",
  "dependencies": [
    "react-markdown@^10.1.0",
    "remark-gfm@^4.0.1"
  ],
  "registryDependencies": [
    "@retab/run-card",
    "@retab/segments",
    "@retab/page-ribbon",
    "@retab/segment-legend",
    "@retab/document-source"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/primitive-run-cards.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport ReactMarkdown, { type Components } from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\n\nimport {\n  SEGMENT_PALETTE,\n  segmentsPageCount,\n  type Segment,\n} from \"@/lib/segments\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\nimport { PageRibbon, type RibbonRow } from \"@/components/ui/page-ribbon\";\nimport { RunCard, type RunStatus } from \"@/components/ui/run-card\";\nimport { SegmentLegend } from \"@/components/ui/segment-legend\";\nimport type { Source, SourceAnchor } from \"@/lib/document-source\";\n\n/**\n * Prop-driven primitive run cards — one card per Retab primitive (classify,\n * split, partition, parse, extract), each framing the source document with that\n * primitive's result over the shared {@link RunCard} shell. Presentational: the\n * caller resolves the result (a category, `Segment[]`, markdown, or extract\n * fields) and passes it in, so the same cards render a static demo or a live\n * workflow run's per-block result.\n */\n\n// The sample pages are US-Letter; one page aspect lets coordinate-anchored\n// overlays (the extract source boxes) map onto the cropped thumbnail.\nconst PAGE_ASPECT = 800 / 1036;\n\nexport type PrimitiveRunCardFile = { name: string; type: string };\n\ninterface PrimitiveRunCardBase {\n  /** Source document the run was over. */\n  file: PrimitiveRunCardFile;\n  /** Run lifecycle pill; defaults to \"completed\". */\n  status?: RunStatus | null;\n  /** Makes the whole card a button. */\n  onClick?: () => void;\n}\n\n// ── Classification ────────────────────────────────────────────────────────────\n\nexport interface ClassificationRunCardProps extends PrimitiveRunCardBase {\n  /** Page image rendered behind the result. */\n  previewImageUrl?: string;\n  /**\n   * Custom media rendered in the shared RunCard frame. Use this when a consumer\n   * owns document loading/cropping behavior but still wants the shared\n   * classification footer and run-card shell.\n   */\n  media?: React.ReactNode;\n  /** Aspect ratio for custom media; defaults to the RunCard media frame. */\n  mediaAspectRatio?: number;\n  category: string;\n  reasoning?: string;\n}\n\nexport function ClassificationRunCard({\n  file,\n  previewImageUrl,\n  media,\n  mediaAspectRatio,\n  category,\n  reasoning,\n  status = \"completed\",\n  onClick,\n}: ClassificationRunCardProps) {\n  const defaultMedia = previewImageUrl ? (\n    <FillThumbnail file={file} src={previewImageUrl} />\n  ) : undefined;\n\n  return (\n    <RunCard\n      file={file}\n      status={status ?? undefined}\n      onClick={onClick}\n      previewAspectRatio={mediaAspectRatio}\n      media={media ?? defaultMedia}\n    >\n      <div className=\"space-y-2 p-3\">\n        <div className=\"bg-background inline-flex min-h-7 max-w-full items-center rounded-md border px-2.5 text-sm font-medium\">\n          <span className=\"truncate\">{category}</span>\n        </div>\n        {reasoning ? (\n          <p className=\"text-muted-foreground line-clamp-3 text-xs\">\n            {reasoning}\n          </p>\n        ) : null}\n      </div>\n    </RunCard>\n  );\n}\n\n// ── Split ─────────────────────────────────────────────────────────────────────\n\nexport interface SplitRunCardProps extends PrimitiveRunCardBase {\n  /** First-page image shown beside the page rail. */\n  previewImageUrl: string;\n  /** Subdocuments as the shared segment model. */\n  segments: Segment[];\n}\n\nexport function SplitRunCard({\n  file,\n  previewImageUrl,\n  segments,\n  status = \"completed\",\n  onClick,\n}: SplitRunCardProps) {\n  return (\n    <RunCard\n      file={file}\n      status={status ?? undefined}\n      onClick={onClick}\n      media={\n        <SplitMiniViewer\n          file={file}\n          previewImageUrl={previewImageUrl}\n          segments={segments}\n        />\n      }\n    >\n      <SegmentLegend\n        variant=\"plain\"\n        density=\"compact\"\n        columns={2}\n        segments={segments}\n      />\n    </RunCard>\n  );\n}\n\n/**\n * A mini split viewer: the document's first page with a vertical, color-keyed\n * page rail beside it — each subdocument is a colored block spanning its page\n * range, shrunk to fit the card's media frame.\n */\nfunction SplitMiniViewer({\n  file,\n  previewImageUrl,\n  segments,\n}: {\n  file: PrimitiveRunCardFile;\n  previewImageUrl: string;\n  segments: Segment[];\n}) {\n  const pageCount = segmentsPageCount(segments);\n\n  return (\n    <div className=\"bg-muted/30 flex size-full\">\n      <div className=\"bg-background flex h-full shrink-0 border-r px-2 py-3\">\n        <PageRibbon\n          orientation=\"vertical\"\n          rows={[{ id: \"split\", segments }]}\n          pageCount={pageCount}\n          rowThickness={12}\n        />\n      </div>\n      <div className=\"relative min-w-0 flex-1\">\n        <FillThumbnail file={file} src={previewImageUrl} />\n      </div>\n    </div>\n  );\n}\n\n// ── Partition ─────────────────────────────────────────────────────────────────\n\nexport interface PartitionRunCardProps extends PrimitiveRunCardBase {\n  previewImageUrl: string;\n  /** Keyed chunks as the shared segment model. */\n  segments: Segment[];\n}\n\nexport function PartitionRunCard({\n  file,\n  previewImageUrl,\n  segments,\n  status = \"completed\",\n  onClick,\n}: PartitionRunCardProps) {\n  const pageCount = segmentsPageCount(segments);\n  // One ribbon lane per chunk over a shared page axis, so chunks that share a\n  // page (e.g. method + experiments on page 4) read as an overlap.\n  const rows: RibbonRow[] = segments.map((segment) => ({\n    id: segment.id,\n    label: segment.label,\n    segments: [segment],\n  }));\n\n  return (\n    <RunCard\n      file={file}\n      status={status ?? undefined}\n      onClick={onClick}\n      media={<FillThumbnail file={file} src={previewImageUrl} />}\n    >\n      <div className=\"space-y-2\">\n        <SegmentLegend\n          variant=\"plain\"\n          density=\"compact\"\n          columns={2}\n          segments={segments}\n        />\n        <PageRibbon\n          orientation=\"horizontal\"\n          rows={rows}\n          pageCount={pageCount}\n          rowThickness={7}\n        />\n      </div>\n    </RunCard>\n  );\n}\n\n// ── Parse ─────────────────────────────────────────────────────────────────────\n\n// Card-sized renderers for the parsed markdown — headings and a GFM table,\n// scaled down to read inside the thumbnail frame.\nconst PARSE_MARKDOWN_COMPONENTS: Components = {\n  h1: (props) => (\n    <h1 className=\"text-foreground mb-1 text-[11px] font-bold\" {...props} />\n  ),\n  h2: (props) => (\n    <h2\n      className=\"text-foreground mt-2 mb-1 text-[10px] font-semibold\"\n      {...props}\n    />\n  ),\n  p: (props) => (\n    <p className=\"text-muted-foreground mb-1 text-[9px]\" {...props} />\n  ),\n  table: (props) => (\n    <table className=\"mb-1 w-full border-collapse text-[8px]\" {...props} />\n  ),\n  th: (props) => (\n    <th\n      className=\"border-border bg-muted/50 border px-1 py-0.5 text-left font-medium\"\n      {...props}\n    />\n  ),\n  td: (props) => (\n    <td\n      className=\"border-border text-muted-foreground border px-1 py-0.5\"\n      {...props}\n    />\n  ),\n};\n\nexport interface ParseRunCardProps extends PrimitiveRunCardBase {\n  /** Parsed markdown to preview in the card frame. */\n  markdown: string;\n}\n\nexport function ParseRunCard({\n  file,\n  markdown,\n  status = \"completed\",\n  onClick,\n}: ParseRunCardProps) {\n  return (\n    <RunCard\n      file={file}\n      status={status ?? undefined}\n      onClick={onClick}\n      media={\n        <div className=\"bg-card size-full overflow-hidden p-3 leading-snug\">\n          <ReactMarkdown\n            remarkPlugins={[remarkGfm]}\n            components={PARSE_MARKDOWN_COMPONENTS}\n          >\n            {markdown}\n          </ReactMarkdown>\n        </div>\n      }\n    />\n  );\n}\n\n// ── Extract ───────────────────────────────────────────────────────────────────\n\nexport interface ExtractRunCardField {\n  key: string;\n  /** Where the value came from — its source box is drawn over the page. */\n  source: Source;\n  /**\n   * Field label/value for the footer list. Omit both for a boxes-only card that\n   * shows just a count — the canvas-card style, where the source geometry is\n   * known but the field name/value isn't projected.\n   */\n  label?: string;\n  value?: string;\n  /** Overlay/legend color; defaults to a palette color by field order. */\n  color?: string;\n}\n\nexport interface ExtractRunCardProps extends PrimitiveRunCardBase {\n  previewImageUrl?: string;\n  /**\n   * Custom media rendered in the shared RunCard frame. Use this when a consumer\n   * owns document loading/cropping/highlight behavior but still wants the shared\n   * extract footer and run-card shell.\n   */\n  media?: React.ReactNode;\n  /** Aspect ratio for custom media; defaults to the RunCard media frame. */\n  mediaAspectRatio?: number;\n  fields: ExtractRunCardField[];\n  /**\n   * Footer count for the boxes-only card (fields without labels). Defaults to\n   * the number of fields.\n   */\n  fieldCount?: number;\n  /** How many labeled fields to list; the rest collapse to \"+N more\". */\n  maxFields?: number;\n  className?: string;\n}\n\nexport function ExtractRunCard({\n  file,\n  previewImageUrl,\n  media,\n  mediaAspectRatio,\n  fields,\n  fieldCount,\n  maxFields = 3,\n  status = \"completed\",\n  onClick,\n  className,\n}: ExtractRunCardProps) {\n  const tinted = fields.map((field, i) => ({\n    ...field,\n    color: field.color ?? SEGMENT_PALETTE[i % SEGMENT_PALETTE.length],\n  }));\n  // Labeled fields list in the footer; box-only fields collapse to a count\n  // chip (the dashboard's canvas-card style — geometry known, names not).\n  const labeled = tinted.filter((field) => field.label != null);\n  const shown = labeled.slice(0, maxFields);\n  const count = fieldCount ?? tinted.length;\n  const rest = Math.max(0, count - shown.length);\n  const defaultMedia = previewImageUrl ? (\n    <div className=\"relative size-full overflow-hidden\">\n      {/* The page sits on a full-page-aspect layer so each field's source\n          box can use its raw normalized coordinates; the 16/10 frame crops\n          it from the top. */}\n      <div\n        className=\"absolute inset-x-0 top-0\"\n        style={{ aspectRatio: String(PAGE_ASPECT) }}\n      >\n        <FileThumbnail\n          file={file}\n          previewImageUrl={previewImageUrl}\n          previewAspectRatio={PAGE_ASPECT}\n          className=\"absolute inset-0 size-full rounded-none border-0\"\n        />\n        {tinted.map((field) => {\n          const box = anchorBox(field.source.anchor);\n          if (!box) return null;\n          return (\n            <span\n              key={field.key}\n              className=\"absolute rounded-[2px]\"\n              style={{\n                left: `${box.left * 100}%`,\n                top: `${box.top * 100}%`,\n                width: `${box.width * 100}%`,\n                height: `${box.height * 100}%`,\n                backgroundColor: withAlpha(field.color, 0.25),\n                boxShadow: `0 0 0 1.5px ${field.color}`,\n              }}\n              title={\n                field.label && field.value\n                  ? `${field.label}: ${field.value}`\n                  : (field.label ?? field.key)\n              }\n            />\n          );\n        })}\n      </div>\n    </div>\n  ) : undefined;\n\n  return (\n    <RunCard\n      file={file}\n      status={status ?? undefined}\n      onClick={onClick}\n      previewAspectRatio={mediaAspectRatio}\n      media={media ?? defaultMedia}\n      className={className}\n    >\n      {shown.length > 0 ? (\n        <div className=\"flex flex-col gap-1\">\n          {shown.map((field) => (\n            <span\n              key={field.key}\n              className=\"flex items-center gap-1.5 text-[11px]\"\n            >\n              <span\n                className=\"h-3 w-5 shrink-0 rounded-[2px]\"\n                style={{ backgroundColor: field.color }}\n              />\n              <span className=\"text-muted-foreground shrink-0\">\n                {field.label}\n              </span>\n              <span className=\"ml-auto truncate font-medium tabular-nums\">\n                {field.value}\n              </span>\n            </span>\n          ))}\n          {rest > 0 ? (\n            <span className=\"text-muted-foreground text-[10px]\">\n              +{rest} more fields\n            </span>\n          ) : null}\n        </div>\n      ) : (\n        <span className=\"text-muted-foreground text-[11px]\">\n          <span className=\"text-foreground font-medium tabular-nums\">\n            {count}\n          </span>{\" \"}\n          {count === 1 ? \"field extracted\" : \"fields extracted\"}\n        </span>\n      )}\n    </RunCard>\n  );\n}\n\n// ── Shared overlay primitives ─────────────────────────────────────────────────\n\n/** The normalized [0, 1] page box of a source, or null when it isn't a region. */\nfunction anchorBox(anchor: SourceAnchor) {\n  if (anchor.kind === \"pdf_bbox\" || anchor.kind === \"image_bbox\") {\n    const { left, top, width, height } = anchor;\n    return { left, top, width, height };\n  }\n  return null;\n}\n\n/** A `FileThumbnail` that fills its positioned parent, cropped from the top. */\nfunction FillThumbnail({\n  file,\n  src,\n}: {\n  file: PrimitiveRunCardFile;\n  src: string;\n}) {\n  return (\n    <FileThumbnail\n      file={file}\n      previewImageUrl={src}\n      previewClassName=\"object-top\"\n      className=\"absolute inset-0 size-full rounded-none border-0\"\n    />\n  );\n}\n\nfunction withAlpha(hex: string, alpha: number): string {\n  const value = hex.replace(\"#\", \"\");\n  const r = parseInt(value.slice(0, 2), 16);\n  const g = parseInt(value.slice(2, 4), 16);\n  const b = parseInt(value.slice(4, 6), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n",
      "type": "registry:ui",
      "target": "@ui/primitive-run-cards.tsx"
    }
  ],
  "categories": [
    "primitives"
  ],
  "type": "registry:ui"
}