{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pdf-viewer-thumbnails",
  "title": "PDF Viewer Thumbnails",
  "description": "PDF-owned page thumbnail navigation for explicit PdfViewer composition: lazy pdfjs thumbnails, active-page highlight, and click-to-jump.",
  "registryDependencies": [
    "@retab/file-viewer",
    "@retab/pdf-viewer",
    "@retab/utils",
    "@retab/spinner"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/pdf-viewer-thumbnails.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { clearPdfDocumentResource } from \"@/lib/pdf-document-resource\";\nimport { cn } from \"@/lib/utils\";\nimport type { ViewerResource } from \"@/lib/viewer-resource\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\nimport {\n  buildPdfThumbnailLayout,\n  PDF_THUMBNAIL_INITIAL_VIEWPORT_HEIGHT,\n  PDF_THUMBNAIL_OVERSCAN_PX,\n  type PdfThumbnailShape,\n} from \"./pdf-thumbnail-layout\";\nimport {\n  useOptionalFileViewerSidebarMotion,\n  type FileViewerSidebarMotion,\n} from \"./file-viewer-renderer-frame\";\nimport { PdfThumbnailRailViewport } from \"./pdf-thumbnail-rail\";\nimport { usePdfViewerThumbnails } from \"./pdf-viewer-context\";\nimport { useIsClient } from \"./use-is-client\";\nimport { usePdfThumbnailDocument } from \"./use-pdf-thumbnail-document\";\nimport { usePdfThumbnailPageMetrics } from \"./use-pdf-thumbnail-page-metrics\";\nimport { usePdfThumbnailWindow } from \"./use-pdf-thumbnail-window\";\nimport { useThumbnailRailFollow } from \"./use-thumbnail-rail-follow\";\nimport { usePdfRenderedPageCache } from \"./pdf-viewer-render-cache\";\nimport { ViewerErrorBoundary } from \"./viewer-error\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nconst PDF_THUMBNAIL_RENDER_RESUME_DELAY_MS = 1400;\nconst PDF_THUMBNAIL_CLOSED_WARM_DELAY_MS = 2200;\n\nexport interface PdfViewerThumbnailsProps {\n  /** Thumbnail image width in CSS pixels. The sidebar shell owns rail width. */\n  thumbnailWidth?: number;\n  /** Preserve page aspect or crop each page preview into a square frame. */\n  thumbnailShape?: PdfThumbnailShape;\n  className?: string;\n}\n\nexport interface PdfThumbnailRailProps {\n  /** Same resource object passed to PdfResourceContent. */\n  resource: ViewerResource;\n  /** 1-based current page; its thumbnail is highlighted. */\n  currentPage?: number | null;\n  /** Click a thumbnail to jump the document to that page. */\n  onSelectPage?: (page: number) => void;\n  /** Thumbnail image width in CSS pixels. The sidebar shell owns rail width. */\n  thumbnailWidth?: number;\n  /** Preserve page aspect or crop each page preview into a square frame. */\n  thumbnailShape?: PdfThumbnailShape;\n  className?: string;\n}\n\nexport function PdfViewerThumbnails({\n  className,\n  thumbnailShape,\n  thumbnailWidth,\n}: PdfViewerThumbnailsProps) {\n  const thumbnails = usePdfViewerThumbnails();\n  const sidebarMotion = useOptionalFileViewerSidebarMotion();\n  const isRenderingSuspended = usePdfThumbnailRenderSuspension(sidebarMotion);\n\n  return (\n    <PdfThumbnailRailShell\n      className={className}\n      currentPage={thumbnails.currentPage}\n      isRenderingSuspended={isRenderingSuspended}\n      onSelectPage={thumbnails.onSelectPage}\n      resource={thumbnails.resource}\n      thumbnailShape={thumbnailShape}\n      thumbnailWidth={thumbnailWidth}\n    />\n  );\n}\n\nexport function PdfThumbnailRail({\n  className,\n  currentPage,\n  onSelectPage,\n  resource,\n  thumbnailShape,\n  thumbnailWidth,\n}: PdfThumbnailRailProps) {\n  return (\n    <PdfThumbnailRailShell\n      className={className}\n      currentPage={currentPage}\n      isRenderingSuspended={false}\n      onSelectPage={onSelectPage}\n      resource={resource}\n      thumbnailShape={thumbnailShape}\n      thumbnailWidth={thumbnailWidth}\n    />\n  );\n}\n\nfunction PdfThumbnailRailShell({\n  className,\n  currentPage,\n  isRenderingSuspended,\n  onSelectPage,\n  resource,\n  thumbnailShape,\n  thumbnailWidth,\n}: PdfThumbnailRailProps & {\n  isRenderingSuspended: boolean;\n}) {\n  const isClient = useIsClient();\n\n  if (!isClient) {\n    return <ThumbnailsFallback className={className} />;\n  }\n\n  return (\n    <ViewerErrorBoundary\n      className=\"h-full\"\n      download={resource.originalDownload}\n      format=\"pdf\"\n      onRetry={() => clearPdfDocumentResource(resource.content)}\n      resetKey={resource.keys.resource}\n      sourceKind={resource.sourceKind}\n      variant=\"inline\"\n    >\n      <React.Suspense fallback={<ThumbnailsFallback className={className} />}>\n        <PdfThumbnailRailInner\n          currentPage={currentPage}\n          isRenderingSuspended={isRenderingSuspended}\n          onSelectPage={onSelectPage}\n          resource={resource}\n          thumbnailShape={thumbnailShape}\n          thumbnailWidth={thumbnailWidth}\n          className={cn(\"h-full\", className)}\n        />\n      </React.Suspense>\n    </ViewerErrorBoundary>\n  );\n}\n\nfunction PdfThumbnailRailInner({\n  resource,\n  currentPage,\n  isRenderingSuspended,\n  onSelectPage,\n  thumbnailShape = \"page\",\n  thumbnailWidth = 120,\n  className,\n}: PdfThumbnailRailProps & {\n  isRenderingSuspended: boolean;\n}) {\n  const doc = usePdfThumbnailDocument(resource);\n  const renderCache = usePdfRenderedPageCache(doc);\n  const pageMetrics = usePdfThumbnailPageMetrics(doc, doc);\n  const { metricByPageNumber, pageCount, requestPageMetrics } = pageMetrics;\n  const viewportRef = React.useRef<HTMLDivElement | null>(null);\n  const activeCurrentPage = isRenderingSuspended ? null : currentPage;\n\n  const layout = React.useMemo(\n    () =>\n      buildPdfThumbnailLayout({\n        pageCount,\n        width: thumbnailWidth,\n        shape: thumbnailShape,\n        metricByPageNumber,\n      }),\n    [metricByPageNumber, pageCount, thumbnailShape, thumbnailWidth],\n  );\n  const thumbnailWindow = usePdfThumbnailWindow({\n    layout,\n    viewportRef,\n    overscanPx: PDF_THUMBNAIL_OVERSCAN_PX,\n    initialViewportHeight: PDF_THUMBNAIL_INITIAL_VIEWPORT_HEIGHT,\n  });\n  const follow = useThumbnailRailFollow({\n    currentPage: activeCurrentPage,\n    layout,\n    viewportRef,\n    resetKey: doc,\n  });\n  useKeyedMountEffect(\n    joinEffectKey([\n      activeCurrentPage,\n      isRenderingSuspended,\n      layout.pageCount,\n      requestPageMetrics,\n      thumbnailWindow.visibleItems,\n    ]),\n    () => {\n      if (isRenderingSuspended) return;\n      requestPageMetrics(\n        getRequestedThumbnailMetricPages({\n          currentPage: activeCurrentPage,\n          pageCount: layout.pageCount,\n          visibleItems: thumbnailWindow.visibleItems,\n        }),\n      );\n    },\n  );\n\n  return (\n    <PdfThumbnailRailViewport\n      doc={doc}\n      documentKey={resource.content.key}\n      isRenderingSuspended={isRenderingSuspended}\n      layout={layout}\n      visibleItems={thumbnailWindow.visibleItems}\n      viewportHeight={thumbnailWindow.viewportHeight}\n      currentPage={activeCurrentPage}\n      viewportRef={viewportRef}\n      onSelectPage={onSelectPage}\n      onPageActivate={follow.onPageActivate}\n      onPointerEnter={follow.onPointerEnter}\n      onPointerLeave={follow.onPointerLeave}\n      onScroll={follow.onScroll}\n      className={className}\n      renderCache={renderCache}\n    />\n  );\n}\n\nfunction usePdfThumbnailRenderSuspension(\n  sidebarMotion: FileViewerSidebarMotion | null,\n) {\n  const isMotionManaged = sidebarMotion?.isMotionManaged === true;\n  const isSidebarOpen = sidebarMotion?.isSidebarOpen === true;\n  const isSidebarInteractive = sidebarMotion?.isSidebarInteractive === true;\n  const isSidebarTransitioning = sidebarMotion?.isSidebarTransitioning === true;\n  const [isOpenSettling, setIsOpenSettling] = React.useState(false);\n  const [isClosedWarmReleased, setIsClosedWarmReleased] = React.useState(false);\n\n  useKeyedMountEffect(\n    joinEffectKey([\n      isMotionManaged,\n      isOpenSettling,\n      isSidebarInteractive,\n      isSidebarOpen,\n      isSidebarTransitioning,\n    ]),\n    () => {\n      if (!isMotionManaged) {\n        setIsOpenSettling(false);\n        setIsClosedWarmReleased(false);\n        return;\n      }\n\n      if (!isSidebarOpen) {\n        setIsOpenSettling(false);\n        setIsClosedWarmReleased(false);\n        if (isSidebarTransitioning) return;\n\n        const timer = window.setTimeout(() => {\n          setIsClosedWarmReleased(true);\n        }, PDF_THUMBNAIL_CLOSED_WARM_DELAY_MS);\n\n        return () => window.clearTimeout(timer);\n      }\n\n      setIsClosedWarmReleased(false);\n\n      if (isSidebarTransitioning || !isSidebarInteractive) {\n        setIsOpenSettling(true);\n        return;\n      }\n\n      if (!isOpenSettling) return;\n\n      const timer = window.setTimeout(() => {\n        setIsOpenSettling(false);\n      }, PDF_THUMBNAIL_RENDER_RESUME_DELAY_MS);\n\n      return () => window.clearTimeout(timer);\n    },\n  );\n\n  if (!isMotionManaged) return false;\n  if (!isSidebarOpen) return !isClosedWarmReleased;\n  if (isSidebarTransitioning || !isSidebarInteractive) return true;\n  return isOpenSettling;\n}\n\nfunction getRequestedThumbnailMetricPages({\n  currentPage,\n  pageCount,\n  visibleItems,\n}: {\n  currentPage: number | null | undefined;\n  pageCount: number;\n  visibleItems: readonly { pageNumber: number }[];\n}) {\n  const pageNumbers = new Set<number>();\n  for (const item of visibleItems) pageNumbers.add(item.pageNumber);\n  if (\n    currentPage != null &&\n    Number.isInteger(currentPage) &&\n    currentPage >= 1 &&\n    currentPage <= pageCount\n  ) {\n    pageNumbers.add(currentPage);\n  }\n\n  return pageNumbers;\n}\n\nfunction ThumbnailsFallback({ className }: { className?: string }) {\n  return (\n    <div\n      className={cn(\n        \"bg-muted/30 flex h-full items-center justify-center\",\n        className,\n      )}\n    >\n      <Spinner className=\"text-muted-foreground size-4\" />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pdf-viewer-thumbnails.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/pdf-thumbnail-layout.ts",
      "content": "export const PDF_THUMBNAIL_OVERSCAN_PX = 1000;\nexport const PDF_THUMBNAIL_INITIAL_VIEWPORT_HEIGHT = 680;\nexport const PDF_THUMBNAIL_DEFAULT_ASPECT = 4 / 3;\nexport const PDF_THUMBNAIL_LABEL_AND_GAP_HEIGHT = 22;\n\nexport type PdfThumbnailShape = \"page\" | \"square\";\n\nexport interface PdfThumbnailPageMetric {\n  pageNumber: number;\n  width: number;\n  height: number;\n}\n\nexport interface PdfThumbnailLayoutItem {\n  pageNumber: number;\n  pageIndex: number;\n  top: number;\n  height: number;\n  imageWidth: number;\n  imageHeight: number;\n}\n\nexport interface PdfThumbnailLayout {\n  pageCount: number;\n  width: number;\n  shape: PdfThumbnailShape;\n  estimatedImageHeight: number;\n  estimatedItemHeight: number;\n  labelAndGapHeight: number;\n  metricByPageNumber: ReadonlyMap<number, PdfThumbnailPageMetric>;\n  prefixHeightDeltas: readonly PdfThumbnailHeightDelta[];\n  totalHeight: number;\n}\n\nexport interface PdfThumbnailRenderedWindowItem\n  extends PdfThumbnailLayoutItem {\n  windowTop: number;\n}\n\nexport interface PdfThumbnailRenderedWindow {\n  afterHeight: number;\n  beforeHeight: number;\n  height: number;\n  items: readonly PdfThumbnailRenderedWindowItem[];\n  renderedBottom: number;\n  renderedTop: number;\n  stickyInset: number;\n}\n\nexport interface PdfThumbnailPixelWindow {\n  bottom: number;\n  top: number;\n}\n\ninterface PdfThumbnailHeightDelta {\n  pageNumber: number;\n  prefixDeltaAfterPage: number;\n}\n\nexport function buildPdfThumbnailLayout({\n  pageCount,\n  width,\n  metricByPageNumber,\n  labelAndGapHeight = PDF_THUMBNAIL_LABEL_AND_GAP_HEIGHT,\n  shape = \"page\",\n}: {\n  pageCount: number;\n  width: number;\n  metricByPageNumber?: ReadonlyMap<number, PdfThumbnailPageMetric>;\n  labelAndGapHeight?: number;\n  shape?: PdfThumbnailShape;\n}): PdfThumbnailLayout {\n  const safePageCount = normalizePageCount(pageCount);\n  const safeWidth = normalizeWidth(width);\n  const safeLabelAndGapHeight = normalizeSize(labelAndGapHeight, 0);\n  const safeMetricByPageNumber = metricByPageNumber ?? new Map();\n  const estimatedImageHeight = getThumbnailImageHeight({\n    width: safeWidth,\n    metric: safeMetricByPageNumber.get(1),\n    shape,\n  });\n  const estimatedItemHeight =\n    Math.ceil(estimatedImageHeight) + safeLabelAndGapHeight;\n  const prefixHeightDeltas = buildPrefixHeightDeltas({\n    pageCount: safePageCount,\n    width: safeWidth,\n    shape,\n    estimatedItemHeight,\n    metricByPageNumber: safeMetricByPageNumber,\n    labelAndGapHeight: safeLabelAndGapHeight,\n  });\n  const totalHeight =\n    safePageCount * estimatedItemHeight +\n    (prefixHeightDeltas.at(-1)?.prefixDeltaAfterPage ?? 0);\n\n  return {\n    pageCount: safePageCount,\n    width: safeWidth,\n    shape,\n    estimatedImageHeight,\n    estimatedItemHeight,\n    labelAndGapHeight: safeLabelAndGapHeight,\n    metricByPageNumber: safeMetricByPageNumber,\n    prefixHeightDeltas,\n    totalHeight,\n  };\n}\n\nexport function getPdfThumbnailLayoutItem(\n  layout: PdfThumbnailLayout,\n  pageNumber: number,\n): PdfThumbnailLayoutItem | null {\n  const normalizedPage = normalizeThumbnailPage(pageNumber, layout.pageCount);\n  if (normalizedPage == null) return null;\n\n  const metric = layout.metricByPageNumber.get(normalizedPage);\n  const imageHeight = getThumbnailImageHeight({\n    width: layout.width,\n    metric,\n    shape: layout.shape,\n  });\n  const height = Math.ceil(imageHeight) + layout.labelAndGapHeight;\n\n  return {\n    pageNumber: normalizedPage,\n    pageIndex: normalizedPage - 1,\n    top:\n      (normalizedPage - 1) * layout.estimatedItemHeight +\n      getPrefixHeightDeltaBeforePage(layout, normalizedPage),\n    height,\n    imageWidth: layout.width,\n    imageHeight,\n  };\n}\n\nexport function getVisiblePdfThumbnailItems({\n  layout,\n  scrollTop,\n  viewportHeight,\n  overscanPx,\n}: {\n  layout: PdfThumbnailLayout;\n  scrollTop: number;\n  viewportHeight: number;\n  overscanPx: number;\n}) {\n  if (layout.pageCount === 0) return [];\n\n  const window = getPdfThumbnailPixelWindow({\n    layout,\n    scrollTop,\n    viewportHeight,\n    overscanPx,\n  });\n  const firstVisiblePage = findPdfThumbnailPageByOffset(layout, window.top);\n  const lastVisiblePage = findPdfThumbnailPageByOffset(layout, window.bottom);\n  const items: PdfThumbnailLayoutItem[] = [];\n\n  for (\n    let pageNumber = firstVisiblePage;\n    pageNumber <= lastVisiblePage;\n    pageNumber += 1\n  ) {\n    const item = getPdfThumbnailLayoutItem(layout, pageNumber);\n    if (item) items.push(item);\n  }\n\n  return items;\n}\n\nexport function getPdfThumbnailPixelWindow({\n  layout,\n  scrollTop,\n  viewportHeight,\n  overscanPx,\n}: {\n  layout: PdfThumbnailLayout;\n  scrollTop: number;\n  viewportHeight: number;\n  overscanPx: number;\n}): PdfThumbnailPixelWindow {\n  const scrollHeight = normalizeSize(layout.totalHeight, 0);\n  const safeViewportHeight = normalizeSize(viewportHeight, 0);\n  const safeOverscanPx = normalizeSize(overscanPx, 0);\n  const windowHeight = safeViewportHeight + safeOverscanPx * 2;\n\n  if (windowHeight >= scrollHeight) {\n    return {\n      bottom: scrollHeight,\n      top: 0,\n    };\n  }\n\n  const scrollCenter =\n    Math.max(0, finiteNumber(scrollTop)) + safeViewportHeight / 2;\n  let top = scrollCenter - windowHeight / 2;\n  let bottom = top + windowHeight;\n\n  if (top < 0) {\n    top = 0;\n    bottom = windowHeight;\n  }\n  if (bottom > scrollHeight) {\n    bottom = scrollHeight;\n    top = scrollHeight - windowHeight;\n  }\n\n  return {\n    bottom: Math.ceil(Math.max(bottom, top)),\n    top: Math.floor(Math.max(0, top)),\n  };\n}\n\nexport function getPdfThumbnailRenderedWindow({\n  layout,\n  visibleItems,\n  viewportHeight,\n}: {\n  layout: PdfThumbnailLayout;\n  visibleItems: readonly PdfThumbnailLayoutItem[];\n  viewportHeight: number;\n}): PdfThumbnailRenderedWindow | null {\n  if (visibleItems.length === 0 || layout.totalHeight <= 0) return null;\n\n  const safeViewportHeight = Math.max(1, normalizeSize(viewportHeight, 1));\n  let renderedTop = layout.totalHeight;\n  let renderedBottom = 0;\n\n  for (const item of visibleItems) {\n    renderedTop = Math.min(renderedTop, item.top);\n    renderedBottom = Math.max(renderedBottom, item.top + item.height);\n  }\n\n  renderedTop = clamp(renderedTop, 0, layout.totalHeight);\n  renderedBottom = clamp(\n    Math.max(renderedTop, renderedBottom),\n    renderedTop,\n    layout.totalHeight,\n  );\n\n  const height = renderedBottom - renderedTop;\n\n  return {\n    afterHeight: Math.max(0, layout.totalHeight - renderedBottom),\n    beforeHeight: renderedTop,\n    height,\n    items: visibleItems.map((item) => ({\n      ...item,\n      windowTop: item.top - renderedTop,\n    })),\n    renderedBottom,\n    renderedTop,\n    stickyInset: -Math.max(0, height - safeViewportHeight),\n  };\n}\n\nexport function findPdfThumbnailPageByOffset(\n  layout: PdfThumbnailLayout,\n  offset: number,\n) {\n  const safeOffset = Math.max(0, offset);\n  let low = 1;\n  let high = layout.pageCount;\n  let result = 1;\n\n  while (low <= high) {\n    const pageNumber = Math.floor((low + high) / 2);\n    const item = getPdfThumbnailLayoutItem(layout, pageNumber);\n    if (!item) break;\n\n    if (item.top + item.height >= safeOffset) {\n      result = pageNumber;\n      high = pageNumber - 1;\n    } else {\n      low = pageNumber + 1;\n    }\n  }\n\n  return result;\n}\n\nexport function normalizeThumbnailPage(\n  page: number | null | undefined,\n  pageCount: number,\n): number | null {\n  return page != null &&\n    Number.isInteger(page) &&\n    page >= 1 &&\n    page <= pageCount\n    ? page\n    : null;\n}\n\nfunction getThumbnailImageHeight({\n  width,\n  metric,\n  shape,\n}: {\n  width: number;\n  metric: PdfThumbnailPageMetric | undefined;\n  shape: PdfThumbnailShape;\n}) {\n  if (shape === \"square\") return width;\n\n  if (\n    metric &&\n    Number.isFinite(metric.width) &&\n    Number.isFinite(metric.height) &&\n    metric.width > 0 &&\n    metric.height > 0\n  ) {\n    return width * (metric.height / metric.width);\n  }\n\n  return width * PDF_THUMBNAIL_DEFAULT_ASPECT;\n}\n\nfunction buildPrefixHeightDeltas({\n  pageCount,\n  width,\n  shape,\n  estimatedItemHeight,\n  metricByPageNumber,\n  labelAndGapHeight,\n}: {\n  pageCount: number;\n  width: number;\n  shape: PdfThumbnailShape;\n  estimatedItemHeight: number;\n  metricByPageNumber: ReadonlyMap<number, PdfThumbnailPageMetric>;\n  labelAndGapHeight: number;\n}) {\n  let prefixDeltaAfterPage = 0;\n  const prefixHeightDeltas: PdfThumbnailHeightDelta[] = [];\n\n  for (const [pageNumber, metric] of [...metricByPageNumber.entries()].sort(\n    ([left], [right]) => left - right,\n  )) {\n    if (normalizeThumbnailPage(pageNumber, pageCount) == null) continue;\n\n    const measuredItemHeight =\n      Math.ceil(getThumbnailImageHeight({ width, metric, shape })) +\n      labelAndGapHeight;\n    const delta = measuredItemHeight - estimatedItemHeight;\n    if (delta === 0) continue;\n\n    prefixDeltaAfterPage += delta;\n    prefixHeightDeltas.push({ pageNumber, prefixDeltaAfterPage });\n  }\n\n  return prefixHeightDeltas;\n}\n\nfunction getPrefixHeightDeltaBeforePage(\n  layout: PdfThumbnailLayout,\n  pageNumber: number,\n) {\n  let low = 0;\n  let high = layout.prefixHeightDeltas.length - 1;\n  let result = 0;\n\n  while (low <= high) {\n    const mid = Math.floor((low + high) / 2);\n    const delta = layout.prefixHeightDeltas[mid];\n\n    if (delta.pageNumber < pageNumber) {\n      result = delta.prefixDeltaAfterPage;\n      low = mid + 1;\n    } else {\n      high = mid - 1;\n    }\n  }\n\n  return result;\n}\n\nfunction normalizePageCount(pageCount: number) {\n  return Number.isInteger(pageCount) && pageCount > 0 ? pageCount : 0;\n}\n\nfunction normalizeWidth(width: number) {\n  return normalizeSize(width, 0);\n}\n\nfunction normalizeSize(value: number, fallback: number) {\n  return Number.isFinite(value) && value > 0 ? value : fallback;\n}\n\nfunction finiteNumber(value: number) {\n  return Number.isFinite(value) ? value : 0;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n",
      "type": "registry:ui",
      "target": "@ui/pdf-thumbnail-layout.ts"
    },
    {
      "path": "registry/new-york-v4/ui/use-pdf-thumbnail-document.ts",
      "content": "\"use client\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport {\n  readPdfDocumentResource,\n  releasePdfDocumentResource,\n  retainPdfDocumentResource,\n} from \"@/lib/pdf-document-resource\";\nimport type { ViewerResource } from \"@/lib/viewer-resource\";\n\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport function usePdfThumbnailDocument(resource: ViewerResource) {\n  const content = resource.content;\n  const doc = readPdfDocumentResource(content);\n\n  const retainEffectKey = joinEffectKey([\n    \"pdf-thumbnail-document\",\n    content.sourceKind,\n    content.key,\n    doc,\n  ]);\n  useKeyedMountEffect(retainEffectKey, () => {\n    retainPdfDocumentResource(content, doc);\n    return () => releasePdfDocumentResource(content, doc);\n  });\n\n  return doc;\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-pdf-thumbnail-document.ts"
    },
    {
      "path": "registry/new-york-v4/ui/use-pdf-thumbnail-page-metrics.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { getPdfPageResource } from \"@/lib/pdf-document-resource\";\nimport type { PdfDocumentProxy } from \"@/lib/pdf-document-types\";\n\nimport { normalizeThumbnailPage } from \"./pdf-thumbnail-layout\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport const PDF_THUMBNAIL_PAGE_METRIC_CONCURRENCY = 4;\n\nexport interface PdfThumbnailPageMetric {\n  pageNumber: number;\n  width: number;\n  height: number;\n}\n\nexport interface PdfThumbnailPageMetrics {\n  pageCount: number;\n  metricByPageNumber: ReadonlyMap<number, PdfThumbnailPageMetric>;\n  requestPageMetrics: (pageNumbers: Iterable<number>) => void;\n  status: \"idle\" | \"loading\";\n}\n\ninterface MetricControllerState {\n  documentKey: unknown;\n  pageCount: number;\n  metricByPageNumber: ReadonlyMap<number, PdfThumbnailPageMetric>;\n  queuedPageNumbers: readonly number[];\n  loadingPageNumbers: ReadonlySet<number>;\n  error: unknown;\n}\n\ntype MetricAction =\n  | { type: \"reset\"; documentKey: unknown; pageCount: number }\n  | {\n      type: \"enqueue\";\n      documentKey: unknown;\n      pageCount: number;\n      pageNumbers: readonly number[];\n    }\n  | { type: \"start\"; documentKey: unknown; pageNumbers: readonly number[] }\n  | { type: \"resolve\"; documentKey: unknown; metric: PdfThumbnailPageMetric }\n  | { type: \"reject\"; documentKey: unknown; error: unknown };\n\nexport function usePdfThumbnailPageMetrics(\n  doc: PdfDocumentProxy,\n  resetKey: unknown,\n): PdfThumbnailPageMetrics {\n  const workerSequenceRef = React.useRef(0);\n  const [state, dispatch] = React.useReducer(\n    metricControllerReducer,\n    undefined,\n    () => createMetricControllerState(resetKey, doc.numPages),\n  );\n  const visibleState = Object.is(state.documentKey, resetKey)\n    ? state\n    : createMetricControllerState(resetKey, doc.numPages);\n  const status = getMetricStatus(visibleState);\n\n  const resetEffectKey = joinEffectKey([\"reset\", doc, resetKey]);\n  useKeyedMountEffect(resetEffectKey, () => {\n    workerSequenceRef.current += 1;\n    dispatch({\n      type: \"reset\",\n      documentKey: resetKey,\n      pageCount: doc.numPages,\n    });\n  });\n\n  const requestPageMetrics = React.useCallback(\n    (pageNumbers: Iterable<number>) => {\n      const requestedPageNumbers = Array.from(pageNumbers)\n        .map((pageNumber) => normalizeThumbnailPage(pageNumber, doc.numPages))\n        .filter((pageNumber): pageNumber is number => pageNumber != null);\n\n      if (requestedPageNumbers.length === 0) return;\n\n      dispatch({\n        type: \"enqueue\",\n        documentKey: resetKey,\n        pageCount: doc.numPages,\n        pageNumbers: requestedPageNumbers,\n      });\n    },\n    [doc.numPages, resetKey],\n  );\n\n  const loadEffectKey = joinEffectKey([\n    \"load\",\n    doc,\n    resetKey,\n    state.documentKey,\n    state.error,\n    state.loadingPageNumbers,\n    state.queuedPageNumbers,\n  ]);\n  useKeyedMountEffect(loadEffectKey, () => {\n    if (!Object.is(state.documentKey, resetKey)) return;\n    if (state.error) return;\n\n    const availableSlotCount =\n      PDF_THUMBNAIL_PAGE_METRIC_CONCURRENCY - state.loadingPageNumbers.size;\n    if (availableSlotCount <= 0) return;\n\n    const startingPageNumbers = state.queuedPageNumbers.slice(\n      0,\n      availableSlotCount,\n    );\n    if (startingPageNumbers.length === 0) return;\n\n    const sequence = workerSequenceRef.current;\n    dispatch({\n      type: \"start\",\n      documentKey: resetKey,\n      pageNumbers: startingPageNumbers,\n    });\n\n    for (const pageNumber of startingPageNumbers) {\n      void getPdfPageResource(doc, pageNumber, { retainRejected: true })\n        .then((page) => {\n          if (workerSequenceRef.current !== sequence) return;\n\n          const viewport = page.getViewport({ scale: 1 });\n          dispatch({\n            type: \"resolve\",\n            documentKey: resetKey,\n            metric: {\n              pageNumber,\n              width: viewport.width,\n              height: viewport.height,\n            },\n          });\n        })\n        .catch((error: unknown) => {\n          if (workerSequenceRef.current !== sequence) return;\n          dispatch({ type: \"reject\", documentKey: resetKey, error });\n        });\n    }\n  });\n\n  if (visibleState.error) throw visibleState.error;\n\n  return {\n    pageCount: visibleState.pageCount,\n    metricByPageNumber: visibleState.metricByPageNumber,\n    requestPageMetrics,\n    status,\n  };\n}\n\nfunction metricControllerReducer(\n  state: MetricControllerState,\n  action: MetricAction,\n): MetricControllerState {\n  switch (action.type) {\n    case \"reset\":\n      return createMetricControllerState(action.documentKey, action.pageCount);\n    case \"enqueue\":\n      return enqueuePageMetrics(state, action);\n    case \"start\":\n      return startPageMetrics(state, action);\n    case \"resolve\":\n      return resolvePageMetric(state, action);\n    case \"reject\":\n      if (!Object.is(state.documentKey, action.documentKey)) return state;\n      return {\n        ...state,\n        queuedPageNumbers: [],\n        loadingPageNumbers: new Set(),\n        error: action.error,\n      };\n  }\n}\n\nfunction createMetricControllerState(\n  documentKey: unknown,\n  pageCount: number,\n): MetricControllerState {\n  return {\n    documentKey,\n    pageCount,\n    metricByPageNumber: new Map(),\n    queuedPageNumbers: [],\n    loadingPageNumbers: new Set(),\n    error: null,\n  };\n}\n\nfunction enqueuePageMetrics(\n  state: MetricControllerState,\n  action: Extract<MetricAction, { type: \"enqueue\" }>,\n): MetricControllerState {\n  const baseState = Object.is(state.documentKey, action.documentKey)\n    ? state\n    : createMetricControllerState(action.documentKey, action.pageCount);\n  const queuedPageNumberSet = new Set(baseState.queuedPageNumbers);\n  const queuedPageNumbers = [...baseState.queuedPageNumbers];\n\n  for (const pageNumber of action.pageNumbers) {\n    if (baseState.metricByPageNumber.has(pageNumber)) continue;\n    if (baseState.loadingPageNumbers.has(pageNumber)) continue;\n    if (queuedPageNumberSet.has(pageNumber)) continue;\n\n    queuedPageNumberSet.add(pageNumber);\n    queuedPageNumbers.push(pageNumber);\n  }\n\n  if (queuedPageNumbers.length === baseState.queuedPageNumbers.length) {\n    return baseState;\n  }\n\n  return {\n    ...baseState,\n    queuedPageNumbers,\n  };\n}\n\nfunction startPageMetrics(\n  state: MetricControllerState,\n  action: Extract<MetricAction, { type: \"start\" }>,\n): MetricControllerState {\n  if (!Object.is(state.documentKey, action.documentKey)) return state;\n\n  const startingPageNumberSet = new Set(action.pageNumbers);\n  const queuedPageNumbers = state.queuedPageNumbers.filter(\n    (pageNumber) => !startingPageNumberSet.has(pageNumber),\n  );\n  const loadingPageNumbers = new Set(state.loadingPageNumbers);\n\n  for (const pageNumber of action.pageNumbers) {\n    if (state.metricByPageNumber.has(pageNumber)) continue;\n    loadingPageNumbers.add(pageNumber);\n  }\n\n  return {\n    ...state,\n    queuedPageNumbers,\n    loadingPageNumbers,\n  };\n}\n\nfunction resolvePageMetric(\n  state: MetricControllerState,\n  action: Extract<MetricAction, { type: \"resolve\" }>,\n): MetricControllerState {\n  if (!Object.is(state.documentKey, action.documentKey)) return state;\n\n  const loadingPageNumbers = new Set(state.loadingPageNumbers);\n  loadingPageNumbers.delete(action.metric.pageNumber);\n\n  const metricByPageNumber = new Map(state.metricByPageNumber);\n  metricByPageNumber.set(action.metric.pageNumber, action.metric);\n\n  return {\n    ...state,\n    metricByPageNumber,\n    loadingPageNumbers,\n  };\n}\n\nfunction getMetricStatus(state: MetricControllerState): \"idle\" | \"loading\" {\n  return state.queuedPageNumbers.length > 0 || state.loadingPageNumbers.size > 0\n    ? \"loading\"\n    : \"idle\";\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-pdf-thumbnail-page-metrics.ts"
    },
    {
      "path": "registry/new-york-v4/ui/use-pdf-thumbnail-window.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\n\nimport {\n  getVisiblePdfThumbnailItems,\n  type PdfThumbnailLayout,\n  type PdfThumbnailLayoutItem,\n} from \"./pdf-thumbnail-layout\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\ninterface PdfThumbnailWindowMetrics {\n  scrollTop: number;\n  viewportHeight: number;\n}\n\nexport interface PdfThumbnailWindow {\n  viewportHeight: number;\n  visibleItems: readonly PdfThumbnailLayoutItem[];\n  totalHeight: number;\n}\n\nexport function usePdfThumbnailWindow({\n  layout,\n  viewportRef,\n  overscanPx,\n  initialViewportHeight,\n}: {\n  layout: PdfThumbnailLayout;\n  viewportRef: React.RefObject<HTMLElement | null>;\n  overscanPx: number;\n  initialViewportHeight: number;\n}): PdfThumbnailWindow {\n  const [metrics, setMetrics] = React.useState<PdfThumbnailWindowMetrics>({\n    scrollTop: 0,\n    viewportHeight: initialViewportHeight,\n  });\n  const metricsRef = React.useRef(metrics);\n\n  const setMeasuredMetrics = React.useCallback(\n    (next: PdfThumbnailWindowMetrics) => {\n      const current = metricsRef.current;\n      if (\n        current.scrollTop === next.scrollTop &&\n        current.viewportHeight === next.viewportHeight\n      ) {\n        return;\n      }\n\n      metricsRef.current = next;\n      setMetrics(next);\n    },\n    [],\n  );\n\n  const viewportEffectKey = joinEffectKey([\n    \"pdf-thumbnail-window\",\n    initialViewportHeight,\n    viewportRef,\n  ]);\n  useKeyedMountEffect(viewportEffectKey, () => {\n    const viewport = viewportRef.current;\n    if (!viewport) return;\n\n    let frame = 0;\n    const read = () => {\n      const scrollTop =\n        Number.isFinite(viewport.scrollTop) && viewport.scrollTop > 0\n          ? viewport.scrollTop\n          : 0;\n      const viewportHeight =\n        Number.isFinite(viewport.clientHeight) && viewport.clientHeight > 0\n          ? viewport.clientHeight\n          : initialViewportHeight;\n\n      setMeasuredMetrics({ scrollTop, viewportHeight });\n    };\n    const scheduleRead = () => {\n      if (frame !== 0) return;\n      frame = window.requestAnimationFrame(() => {\n        frame = 0;\n        read();\n      });\n    };\n    const observer =\n      typeof ResizeObserver !== \"undefined\"\n        ? new ResizeObserver(scheduleRead)\n        : null;\n\n    read();\n    observer?.observe(viewport);\n    viewport.addEventListener(\"scroll\", scheduleRead, { passive: true });\n\n    return () => {\n      viewport.removeEventListener(\"scroll\", scheduleRead);\n      observer?.disconnect();\n      if (frame !== 0) window.cancelAnimationFrame(frame);\n    };\n  });\n\n  return React.useMemo(\n    () => ({\n      viewportHeight: metrics.viewportHeight,\n      visibleItems: getVisiblePdfThumbnailItems({\n        layout,\n        scrollTop: metrics.scrollTop,\n        viewportHeight: metrics.viewportHeight,\n        overscanPx,\n      }),\n      totalHeight: layout.totalHeight,\n    }),\n    [layout, metrics, overscanPx],\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-pdf-thumbnail-window.ts"
    },
    {
      "path": "registry/new-york-v4/ui/use-thumbnail-rail-follow.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\n\nimport {\n  getPdfThumbnailLayoutItem,\n  normalizeThumbnailPage,\n  type PdfThumbnailLayout,\n} from \"./pdf-thumbnail-layout\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport const THUMBNAIL_FOLLOW_MARGIN = 24;\n\ntype ThumbnailFollowSuspension =\n  | { kind: \"none\" }\n  | { kind: \"pointer\" }\n  | { kind: \"user-scroll\" }\n  | { kind: \"rail-navigation\"; targetPage: number };\n\ninterface ThumbnailFollowState {\n  suspension: ThumbnailFollowSuspension;\n  currentPage: number | null;\n  programmaticScrollCount: number;\n}\n\nexport function useThumbnailRailFollow({\n  currentPage,\n  layout,\n  viewportRef,\n  resetKey,\n}: {\n  currentPage: number | null | undefined;\n  layout: PdfThumbnailLayout;\n  viewportRef: React.RefObject<HTMLElement | null>;\n  resetKey: unknown;\n}) {\n  const stateRef = React.useRef<ThumbnailFollowState>({\n    suspension: { kind: \"none\" },\n    currentPage: null,\n    programmaticScrollCount: 0,\n  });\n\n  const scrollPageIntoView = React.useCallback(\n    (page: number, behavior: ScrollBehavior) => {\n      const normalizedPage = normalizeThumbnailPage(page, layout.pageCount);\n      if (normalizedPage == null) return false;\n\n      const viewport = viewportRef.current;\n      if (!viewport) return false;\n\n      const item = getPdfThumbnailLayoutItem(layout, normalizedPage);\n      if (!item) return false;\n\n      const top = item.top - viewport.scrollTop;\n      const bottom = top + item.height;\n      const minTop = THUMBNAIL_FOLLOW_MARGIN;\n      const maxBottom = viewport.clientHeight - THUMBNAIL_FOLLOW_MARGIN;\n      const isAtDocumentStart =\n        item.top <= THUMBNAIL_FOLLOW_MARGIN &&\n        viewport.scrollTop <= THUMBNAIL_FOLLOW_MARGIN;\n\n      if ((top >= minTop || isAtDocumentStart) && bottom <= maxBottom) {\n        return false;\n      }\n\n      const maxScrollTop = Math.max(\n        0,\n        layout.totalHeight - viewport.clientHeight,\n      );\n      const targetTop = Math.min(\n        maxScrollTop,\n        Math.max(0, item.top - viewport.clientHeight / 2 + item.height / 2),\n      );\n\n      if (stateRef.current.suspension.kind !== \"rail-navigation\") {\n        stateRef.current.programmaticScrollCount += 1;\n      }\n      viewport.scrollTo?.({ top: targetTop, behavior });\n      return true;\n    },\n    [layout, viewportRef],\n  );\n\n  const followNow = React.useCallback(() => {\n    const page = normalizeThumbnailPage(currentPage, layout.pageCount);\n    if (page == null) return;\n\n    const state = stateRef.current;\n    if (state.suspension.kind !== \"none\") return;\n\n    scrollPageIntoView(page, \"auto\");\n  }, [currentPage, layout.pageCount, scrollPageIntoView]);\n\n  useKeyedMountEffect(joinEffectKey([\"thumbnail-reset\", resetKey]), () => {\n    const state = stateRef.current;\n    state.suspension = { kind: \"none\" };\n    state.currentPage = null;\n    state.programmaticScrollCount = 0;\n  });\n\n  useKeyedMountEffect(\n    joinEffectKey([\"thumbnail-follow\", followNow, resetKey]),\n    () => {\n      const page = normalizeThumbnailPage(currentPage, layout.pageCount);\n      const state = stateRef.current;\n      const pageChanged = state.currentPage !== page;\n\n      state.currentPage = page;\n      if (state.suspension.kind === \"rail-navigation\") {\n        if (pageChanged || page === state.suspension.targetPage) {\n          state.suspension = { kind: \"none\" };\n          followNow();\n        }\n        return;\n      }\n\n      if (state.suspension.kind === \"user-scroll\" && pageChanged) {\n        state.suspension = { kind: \"none\" };\n      }\n\n      followNow();\n    },\n  );\n\n  const onPointerEnter = React.useCallback(() => {\n    const state = stateRef.current;\n    if (\n      state.suspension.kind === \"user-scroll\" ||\n      state.suspension.kind === \"rail-navigation\"\n    ) {\n      return;\n    }\n    state.suspension = { kind: \"pointer\" };\n  }, []);\n\n  const onPointerLeave = React.useCallback(() => {\n    const state = stateRef.current;\n    if (state.suspension.kind !== \"pointer\") return;\n    state.suspension = { kind: \"none\" };\n    followNow();\n  }, [followNow]);\n\n  const onPageActivate = React.useCallback(\n    (pageNumber: number) => {\n      const targetPage = normalizeThumbnailPage(pageNumber, layout.pageCount);\n      if (targetPage == null) return;\n\n      const state = stateRef.current;\n      state.suspension =\n        state.currentPage === targetPage\n          ? { kind: \"none\" }\n          : { kind: \"rail-navigation\", targetPage };\n      scrollPageIntoView(targetPage, \"smooth\");\n    },\n    [layout.pageCount, scrollPageIntoView],\n  );\n\n  const onScroll = React.useCallback(() => {\n    const state = stateRef.current;\n    if (state.programmaticScrollCount > 0) {\n      state.programmaticScrollCount -= 1;\n      return;\n    }\n\n    if (state.suspension.kind === \"rail-navigation\") return;\n\n    state.suspension = { kind: \"user-scroll\" };\n  }, []);\n\n  return {\n    onPageActivate,\n    onPointerEnter,\n    onPointerLeave,\n    onScroll,\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-thumbnail-rail-follow.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pdf-thumbnail-rail.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { PdfDocumentProxy } from \"@/lib/pdf-document-types\";\nimport { cn } from \"@/lib/utils\";\n\nimport { PdfThumbnailItem } from \"./pdf-thumbnail-item\";\nimport {\n  getPdfThumbnailRenderedWindow,\n  normalizeThumbnailPage,\n  type PdfThumbnailLayout,\n  type PdfThumbnailLayoutItem,\n} from \"./pdf-thumbnail-layout\";\nimport type { PdfRenderedPageCache } from \"./pdf-viewer-render-cache\";\n\nexport function getPdfThumbnailItemId(pageNumber: number) {\n  return `pdf-thumbnail-page-${pageNumber}`;\n}\n\nexport function PdfThumbnailRailViewport({\n  doc,\n  documentKey,\n  isRenderingSuspended = false,\n  layout,\n  visibleItems,\n  viewportHeight,\n  currentPage,\n  viewportRef,\n  onSelectPage,\n  onPageActivate,\n  onPointerEnter,\n  onPointerLeave,\n  onScroll,\n  className,\n  renderCache,\n}: {\n  doc: PdfDocumentProxy;\n  documentKey: string;\n  isRenderingSuspended?: boolean;\n  layout: PdfThumbnailLayout;\n  visibleItems: readonly PdfThumbnailLayoutItem[];\n  viewportHeight: number;\n  currentPage: number | null | undefined;\n  viewportRef: React.RefObject<HTMLDivElement | null>;\n  onSelectPage?: (pageNumber: number) => void;\n  onPageActivate?: (pageNumber: number) => void;\n  onPointerEnter?: () => void;\n  onPointerLeave?: () => void;\n  onScroll?: () => void;\n  className?: string;\n  renderCache?: PdfRenderedPageCache;\n}) {\n  const activePage = normalizeThumbnailPage(currentPage, layout.pageCount);\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLElement>) => {\n      const page = activePage ?? 1;\n      const nextPage = getKeyboardTargetPage({\n        key: event.key,\n        page,\n        pageCount: layout.pageCount,\n      });\n\n      if (nextPage == null) return;\n      event.preventDefault();\n      onPageActivate?.(nextPage);\n      onSelectPage?.(nextPage);\n    },\n    [activePage, layout.pageCount, onPageActivate, onSelectPage],\n  );\n  const handleSelectPage = React.useCallback(\n    (pageNumber: number) => {\n      onPageActivate?.(pageNumber);\n      onSelectPage?.(pageNumber);\n    },\n    [onPageActivate, onSelectPage],\n  );\n  const renderedWindow = React.useMemo(\n    () =>\n      getPdfThumbnailRenderedWindow({\n        layout,\n        visibleItems,\n        viewportHeight,\n      }),\n    [layout, visibleItems, viewportHeight],\n  );\n\n  return (\n    <nav\n      ref={viewportRef}\n      data-slot=\"pdf-viewer-thumbnails\"\n      aria-label=\"PDF pages\"\n      tabIndex={0}\n      onKeyDown={handleKeyDown}\n      onPointerEnter={onPointerEnter}\n      onPointerLeave={onPointerLeave}\n      onScroll={onScroll}\n      className={cn(\n        \"bg-sidebar text-sidebar-foreground h-full overflow-auto p-2\",\n        className,\n      )}\n      style={{ overflowAnchor: \"none\" }}\n    >\n      <div\n        data-slot=\"pdf-thumbnail-document\"\n        className=\"relative w-full\"\n        style={{\n          contain: \"layout style\",\n          height: layout.totalHeight,\n        }}\n      >\n        {renderedWindow ? (\n          <>\n            <div\n              aria-hidden\n              data-slot=\"pdf-thumbnail-window-before\"\n              style={{\n                contain: \"layout size\",\n                height: renderedWindow.beforeHeight,\n              }}\n            />\n            <div\n              data-slot=\"pdf-thumbnail-sticky-window\"\n              className=\"sticky w-full\"\n              style={{\n                bottom: renderedWindow.stickyInset,\n                contain: \"layout style inline-size\",\n                height: renderedWindow.height,\n                isolation: \"isolate\",\n                top: renderedWindow.stickyInset,\n              }}\n            >\n              <ol\n                data-slot=\"pdf-thumbnail-window\"\n                className=\"relative w-full\"\n                style={{\n                  contain: \"layout style\",\n                  height: renderedWindow.height,\n                }}\n              >\n                {renderedWindow.items.map((item) => (\n                  <li\n                    key={item.pageNumber}\n                    data-index={item.pageIndex}\n                    className=\"absolute top-0 left-0 flex w-full justify-center pb-2\"\n                    style={{\n                      height: item.height,\n                      transform: `translateY(${item.windowTop}px)`,\n                    }}\n                  >\n                    <PdfThumbnailItem\n                      doc={doc}\n                      documentKey={documentKey}\n                      item={item}\n                      active={activePage === item.pageNumber}\n                      isRenderingSuspended={isRenderingSuspended}\n                      itemId={getPdfThumbnailItemId(item.pageNumber)}\n                      onSelectPage={handleSelectPage}\n                      renderCache={renderCache}\n                    />\n                  </li>\n                ))}\n              </ol>\n            </div>\n            <div\n              aria-hidden\n              data-slot=\"pdf-thumbnail-window-after\"\n              style={{\n                contain: \"layout size\",\n                height: renderedWindow.afterHeight,\n              }}\n            />\n          </>\n        ) : null}\n      </div>\n    </nav>\n  );\n}\n\nfunction getKeyboardTargetPage({\n  key,\n  page,\n  pageCount,\n}: {\n  key: string;\n  page: number;\n  pageCount: number;\n}) {\n  switch (key) {\n    case \"ArrowUp\":\n      return Math.max(1, page - 1);\n    case \"ArrowDown\":\n      return Math.min(pageCount, page + 1);\n    case \"Home\":\n      return 1;\n    case \"End\":\n      return pageCount;\n    default:\n      return null;\n  }\n}\n",
      "type": "registry:ui",
      "target": "@ui/pdf-thumbnail-rail.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/pdf-thumbnail-item.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { PdfDocumentProxy } from \"@/lib/pdf-document-types\";\nimport { cn } from \"@/lib/utils\";\n\nimport {\n  PdfThumbnailCanvas,\n  PdfThumbnailSkeleton,\n} from \"./pdf-thumbnail-canvas\";\nimport type { PdfThumbnailLayoutItem } from \"./pdf-thumbnail-layout\";\nimport type { PdfRenderedPageCache } from \"./pdf-viewer-render-cache\";\n\nexport function PdfThumbnailItem({\n  doc,\n  documentKey,\n  item,\n  active,\n  isRenderingSuspended = false,\n  itemId,\n  onSelectPage,\n  renderCache,\n}: {\n  doc: PdfDocumentProxy;\n  documentKey: string;\n  item: PdfThumbnailLayoutItem;\n  active: boolean;\n  isRenderingSuspended?: boolean;\n  itemId: string;\n  onSelectPage?: (pageNumber: number) => void;\n  renderCache?: PdfRenderedPageCache;\n}) {\n  return (\n    <button\n      id={itemId}\n      type=\"button\"\n      aria-label={`Page ${item.pageNumber}`}\n      aria-current={active ? \"page\" : undefined}\n      data-active={active}\n      data-page-number={item.pageNumber}\n      onClick={() => onSelectPage?.(item.pageNumber)}\n      className=\"flex flex-shrink-0 flex-col items-center gap-1 outline-none\"\n    >\n      <div\n        className={cn(\n          \"overflow-hidden rounded-sm bg-white ring-2 transition-shadow\",\n          active ? \"ring-primary\" : \"ring-sidebar-border\",\n        )}\n        style={{ width: item.imageWidth, height: item.imageHeight }}\n      >\n        <React.Suspense fallback={<PdfThumbnailSkeleton />}>\n          <PdfThumbnailCanvas\n            doc={doc}\n            documentKey={documentKey}\n            isRenderingSuspended={isRenderingSuspended}\n            pageNumber={item.pageNumber}\n            renderCache={renderCache}\n            width={item.imageWidth}\n          />\n        </React.Suspense>\n      </div>\n      <span\n        className={cn(\n          \"text-[10px] tabular-nums\",\n          active\n            ? \"text-sidebar-foreground font-semibold\"\n            : \"text-sidebar-foreground/70\",\n        )}\n      >\n        {item.pageNumber}\n      </span>\n    </button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pdf-thumbnail-item.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/pdf-thumbnail-canvas.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { PdfDocumentProxy } from \"@/lib/pdf-document-types\";\nimport { readPdfPageResource } from \"@/lib/pdf-document-resource\";\n\nimport { getPdfCanvasPixelSize } from \"./pdf-viewer-canvas\";\nimport {\n  readPdfRenderedPageCache,\n  writePdfRenderedPageCache,\n  type PdfRenderedPageCache,\n  type PdfRenderedPageSignature,\n} from \"./pdf-viewer-render-cache\";\nimport { toPdfRenderFailedError } from \"./pdf-viewer-render-error\";\n\nconst PDF_THUMBNAIL_MAX_DEVICE_PIXEL_RATIO = 1;\n\nexport function PdfThumbnailCanvas({\n  doc,\n  documentKey,\n  isRenderingSuspended = false,\n  pageNumber,\n  renderCache,\n  width,\n}: {\n  doc: PdfDocumentProxy;\n  documentKey: string;\n  isRenderingSuspended?: boolean;\n  pageNumber: number;\n  renderCache?: PdfRenderedPageCache;\n  width: number;\n}) {\n  const page = readPdfPageResource(doc, pageNumber);\n  const baseViewport = React.useMemo(\n    () => page.getViewport({ scale: 1 }),\n    [page],\n  );\n  const viewport = React.useMemo(\n    () => page.getViewport({ scale: width / baseViewport.width }),\n    [baseViewport.width, page, width],\n  );\n  const dpr = Math.min(\n    (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1,\n    PDF_THUMBNAIL_MAX_DEVICE_PIXEL_RATIO,\n  );\n  const [renderError, setRenderError] = React.useState<unknown>(null);\n  if (renderError) throw renderError;\n\n  const canvasRef = React.useCallback(\n    (canvas: HTMLCanvasElement | null) => {\n      if (!canvas) return;\n      const context = canvas.getContext(\"2d\");\n      if (!context) {\n        setRenderError(\n          toPdfRenderFailedError(new Error(\"Canvas 2D context unavailable.\")),\n        );\n        return;\n      }\n\n      const renderSignature = {\n        documentKey,\n        pageNumber,\n        scale: width / baseViewport.width,\n        rotation: 0,\n        devicePixelRatio: dpr,\n        viewportWidth: viewport.width,\n        viewportHeight: viewport.height,\n      } satisfies PdfRenderedPageSignature;\n      const canvasWidth = getPdfCanvasPixelSize(viewport.width, dpr);\n      const canvasHeight = getPdfCanvasPixelSize(viewport.height, dpr);\n      const cachedPage = readPdfRenderedPageCache(renderCache, renderSignature);\n\n      resizeCanvas(canvas, canvasWidth, canvasHeight);\n      if (isRenderingSuspended) {\n        markCanvasRenderStatus(canvas, renderSignature, \"suspended\");\n        return;\n      }\n\n      if (cachedPage) {\n        drawCanvasImage(context, cachedPage.canvas, canvasWidth, canvasHeight);\n        markCanvasRenderStatus(canvas, renderSignature, \"rendered\", \"cache\");\n        return;\n      }\n      markCanvasRenderStatus(canvas, renderSignature, \"pending\");\n\n      let task: ReturnType<typeof page.render>;\n      try {\n        task = page.render({\n          canvas,\n          canvasContext: context,\n          viewport,\n          transform: dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined,\n        });\n      } catch (error) {\n        setRenderError(toPdfRenderFailedError(error));\n        return;\n      }\n\n      let isActive = true;\n      let didFinishPdfRender = false;\n      task.promise.then(\n        () => {\n          if (!isActive) return;\n          didFinishPdfRender = true;\n          writePdfRenderedPageCache({\n            cache: renderCache,\n            rendered: renderSignature,\n            sourceCanvas: canvas,\n          });\n          markCanvasRenderStatus(canvas, renderSignature, \"rendered\", \"pdfjs\");\n        },\n        (error) => {\n          if (!isActive) return;\n          didFinishPdfRender = true;\n          markCanvasRenderStatus(canvas, renderSignature, \"failed\");\n          setRenderError(toPdfRenderFailedError(error));\n        },\n      );\n\n      return () => {\n        isActive = false;\n        if (!didFinishPdfRender) {\n          task.cancel();\n          markCanvasRenderStatus(canvas, renderSignature, \"cancelled\");\n        }\n      };\n    },\n    [\n      baseViewport.width,\n      documentKey,\n      dpr,\n      isRenderingSuspended,\n      page,\n      pageNumber,\n      renderCache,\n      viewport,\n      width,\n    ],\n  );\n\n  return (\n    <canvas\n      ref={canvasRef}\n      style={{ width: viewport.width, height: viewport.height }}\n      className=\"block\"\n    />\n  );\n}\n\nfunction resizeCanvas(\n  canvas: HTMLCanvasElement,\n  width: number,\n  height: number,\n) {\n  if (canvas.width !== width) canvas.width = width;\n  if (canvas.height !== height) canvas.height = height;\n}\n\nfunction drawCanvasImage(\n  context: CanvasRenderingContext2D,\n  image: HTMLCanvasElement,\n  width: number,\n  height: number,\n) {\n  if (typeof context.setTransform === \"function\") {\n    context.setTransform(1, 0, 0, 1, 0, 0);\n  }\n  if (typeof context.drawImage === \"function\") {\n    context.drawImage(image, 0, 0, width, height);\n  }\n}\n\nfunction markCanvasRenderStatus(\n  canvas: HTMLCanvasElement,\n  rendered: PdfRenderedPageSignature,\n  status: \"pending\" | \"rendered\" | \"cancelled\" | \"failed\" | \"suspended\",\n  source?: \"cache\" | \"pdfjs\",\n) {\n  canvas.dataset.pdfPageNumber = String(rendered.pageNumber);\n  canvas.dataset.pdfRenderStatus = status;\n  if (source) {\n    canvas.dataset.pdfRenderSource = source;\n  } else {\n    delete canvas.dataset.pdfRenderSource;\n  }\n}\n\nexport function PdfThumbnailSkeleton() {\n  return <div className=\"bg-muted aspect-[3/4] w-full\" />;\n}\n",
      "type": "registry:ui",
      "target": "@ui/pdf-thumbnail-canvas.tsx"
    }
  ],
  "type": "registry:ui"
}