{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-timeline",
  "title": "Page Timeline",
  "description": "A colored page strip that ties a Segment[] to the document; hover/select syncing and click-to-jump.",
  "registryDependencies": [
    "@retab/segments",
    "@retab/segment-interaction",
    "@retab/utils"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/page-timeline.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  getSegmentInteractionState,\n  getSegmentSurfaceProps,\n  scopeSegmentInteraction,\n  type SegmentInteraction,\n} from \"@/lib/segment-interaction\";\nimport {\n  normalizePageCount,\n  segmentDisplayLabel,\n  segmentsPageCount,\n  type Segment,\n} from \"@/lib/segments\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface PageTimelineProps {\n  segments: Segment[];\n  /** Total pages; defaults to the max page across segments. */\n  pageCount?: number;\n  /** Shared preview state. */\n  interaction?: SegmentInteraction;\n  /** 1-based current page to mark (e.g. synced to a PDF scroll position). */\n  currentPage?: number | null;\n  /** Fired when a page cell is clicked. */\n  onSelectPage?: (page: number) => void;\n  /** Fired when a segment-owned page cell is clicked. */\n  onSelect?: (segment: Segment) => void;\n  className?: string;\n}\n\ninterface TimelinePageSegment {\n  segmentIndexes: number[];\n  primarySegment?: Segment;\n  label: string;\n}\n\n/**\n * A horizontal strip of page cells colored by the segment mapped to each page.\n * It ties the legend and sidebar to the document: hovering a cell previews its\n * segment, previewing a segment elsewhere dims unrelated pages, and clicking\n * a cell jumps the document. If multiple segments map to a page, the first\n * segment in document order owns the preview state and the page label calls out\n * the overlap.\n */\nexport function PageTimeline({\n  segments,\n  pageCount,\n  interaction,\n  currentPage,\n  onSelectPage,\n  onSelect,\n  className,\n}: PageTimelineProps) {\n  const total = normalizePageCount(pageCount ?? segmentsPageCount(segments));\n  const segmentIndexesByPage = React.useMemo(\n    () => buildPageSegmentIndexes(segments),\n    [segments],\n  );\n  const segmentByIndex = React.useMemo(\n    () => buildSegmentByIndex(segments),\n    [segments],\n  );\n  const segmentIndexById = React.useMemo(\n    () => buildSegmentIndexById(segments),\n    [segments],\n  );\n  const visibleSegments = React.useMemo(\n    () =>\n      segments.filter((segment) =>\n        segment.pages.some(\n          (page) => Number.isInteger(page) && page >= 1 && page <= total,\n        ),\n      ),\n    [segments, total],\n  );\n  const scopedInteraction = React.useMemo(\n    () =>\n      scopeSegmentInteraction(\n        interaction,\n        visibleSegments.map((segment) => segment.id),\n      ),\n    [interaction, visibleSegments],\n  );\n  const interactionState = React.useMemo(\n    () =>\n      getSegmentInteractionState({\n        segments: visibleSegments,\n        currentPage,\n        interaction: scopedInteraction,\n      }),\n    [currentPage, scopedInteraction, visibleSegments],\n  );\n\n  if (total <= 0) return null;\n  const previewedSegmentIndex = getPreviewedSegmentIndex({\n    previewedSegmentId: interactionState.previewSegmentId,\n    segmentIndexById,\n  });\n\n  return (\n    <div\n      data-slot=\"page-timeline\"\n      onMouseLeave={() => scopedInteraction?.clearPreview()}\n      className={cn(\n        \"flex items-stretch gap-px overflow-hidden rounded-md\",\n        className,\n      )}\n    >\n      {Array.from({ length: total }, (_, i) => {\n        const page = i + 1;\n        const pageSegment = getTimelinePageSegment({\n          page,\n          segmentIndexesByPage,\n          segmentByIndex,\n        });\n        const { segmentIndexes, primarySegment } = pageSegment;\n        const surfaceProps = primarySegment\n          ? getSegmentSurfaceProps({\n              segment: primarySegment,\n              interaction: scopedInteraction,\n              interactionState,\n              isCurrent: currentPage === page,\n              onSelect,\n            })\n          : null;\n        const dimmed =\n          previewedSegmentIndex != null\n            ? !segmentIndexes.includes(previewedSegmentIndex)\n            : false;\n        const isCurrent = currentPage === page;\n        return (\n          <button\n            key={page}\n            type=\"button\"\n            aria-current={isCurrent ? \"page\" : undefined}\n            aria-label={pageSegment.label}\n            title={pageSegment.label}\n            onPointerEnter={surfaceProps?.eventHandlers.onPointerEnter}\n            onPointerLeave={surfaceProps?.eventHandlers.onPointerLeave}\n            onClick={() => {\n              surfaceProps?.eventHandlers.onClick();\n              onSelectPage?.(page);\n            }}\n            data-previewed={surfaceProps?.state.isPreviewed ?? false}\n            data-current={isCurrent}\n            data-highlighted={surfaceProps?.state.isHighlighted ?? isCurrent}\n            className={cn(\n              \"group focus-visible:ring-ring relative h-7 flex-1 transition-opacity focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none\",\n              dimmed ? \"opacity-25\" : \"opacity-100\",\n            )}\n            style={{\n              backgroundColor: primarySegment\n                ? primarySegment.color\n                : \"var(--muted)\",\n            }}\n          >\n            {isCurrent ? (\n              <span className=\"ring-foreground pointer-events-none absolute inset-0 ring-2 ring-inset\" />\n            ) : null}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction buildSegmentByIndex(segments: Segment[]): Map<number, Segment> {\n  const map = new Map<number, Segment>();\n  segments.forEach((segment, index) => map.set(index, segment));\n  return map;\n}\n\nfunction buildSegmentIndexById(segments: Segment[]): Map<string, number> {\n  const map = new Map<string, number>();\n  segments.forEach((segment, index) => map.set(segment.id, index));\n  return map;\n}\n\nfunction buildPageSegmentIndexes(segments: Segment[]): Map<number, number[]> {\n  const owners = new Map<number, number[]>();\n  segments.forEach((segment, index) => {\n    segment.pages.forEach((page) => {\n      if (!Number.isInteger(page) || page <= 0) return;\n      const list = owners.get(page) ?? [];\n      list.push(index);\n      owners.set(page, list);\n    });\n  });\n  return owners;\n}\n\nfunction getTimelinePageSegment({\n  page,\n  segmentIndexesByPage,\n  segmentByIndex,\n}: {\n  page: number;\n  segmentIndexesByPage: Map<number, number[]>;\n  segmentByIndex: Map<number, Segment>;\n}): TimelinePageSegment {\n  const segmentIndexes = segmentIndexesByPage.get(page) ?? [];\n  const primarySegment = segmentIndexes.length\n    ? segmentByIndex.get(segmentIndexes[0])\n    : undefined;\n  const segmentLabel =\n    segmentIndexes.length > 1\n      ? `${segmentIndexes.length} segments`\n      : primarySegment\n        ? segmentDisplayLabel(primarySegment.label)\n        : undefined;\n\n  return {\n    segmentIndexes,\n    primarySegment,\n    label: `Page ${page}${segmentLabel ? ` · ${segmentLabel}` : \"\"}`,\n  };\n}\n\nfunction getPreviewedSegmentIndex({\n  previewedSegmentId,\n  segmentIndexById,\n}: {\n  previewedSegmentId: string | null;\n  segmentIndexById: Map<string, number>;\n}): number | undefined {\n  return previewedSegmentId\n    ? segmentIndexById.get(previewedSegmentId)\n    : undefined;\n}\n",
      "type": "registry:ui",
      "target": "@ui/page-timeline.tsx"
    }
  ],
  "type": "registry:ui"
}