{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pptx-viewer",
  "title": "PPTX Viewer",
  "description": "A canvas-backed PowerPoint viewer rendered entirely client-side (no server conversion): continuous scroll, zoom, rotate, fit-to-width, download, and a per-slide overlay slot. Slides render lazily near the viewport and renders are serialized.",
  "dependencies": [
    "lucide-react@^0.514.0",
    "pptxviewjs@1.1.9",
    "chart.js@^4.5.0"
  ],
  "registryDependencies": [
    "@retab/utils",
    "button",
    "@retab/scroll-area",
    "separator",
    "@retab/skeleton",
    "dropdown-menu",
    "@retab/viewer-controls",
    "@retab/use-keyed-layout-effect",
    "@retab/use-mount-effect"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/pptx-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\nimport {\n  createViewerResource,\n  type ViewerResource,\n} from \"@/lib/viewer-resource\";\n\nimport { getPptxFitScale, getPptxResetKey } from \"./pptx-viewer-core\";\nimport {\n  FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,\n  readFileViewerBeforeLayoutMotionFrame,\n} from \"./file-viewer-elements\";\nimport {\n  captureFileViewerFitWidthAnchorScreenOffset,\n  createFileViewerFitWidthSurfaceMotionResolver,\n  FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY,\n  resolveFileViewerFitWidthMotionAnchorBlock,\n} from \"./file-viewer-fit-width-motion\";\nimport type { FileViewerDocumentSurfaceMotionResolver } from \"./file-viewer-motion-kernel\";\nimport type { FileViewerMotionFrame } from \"./file-viewer-motion-plan\";\nimport { resolveFileViewerRendererLayoutInlineSize } from \"./file-viewer-renderer-contract\";\nimport {\n  useOptionalFileViewerRendererEnvironment,\n  useOptionalFileViewerRendererFrame,\n} from \"./file-viewer-renderer-frame\";\nimport { useReadingFractionRebase } from \"./use-reading-fraction-rebase\";\nimport { PptxViewerFallback } from \"./pptx-viewer-fallback\";\nimport { useRetainedPptxSource } from \"./pptx-viewer-hooks\";\nimport { preloadPptxRenderer } from \"./pptx-viewer-renderer\";\nimport { createPptxScrollActivity } from \"./pptx-viewer-scroll\";\nimport {\n  PPTX_SLIDE_GAP,\n  PPTX_SLIDE_PADDING,\n  PptxSlideScroller,\n} from \"./pptx-viewer-slide\";\nimport { evictPptxSource } from \"./pptx-viewer-source\";\nimport type { PptxViewerProps } from \"./pptx-viewer-types\";\nimport { usePptxViewportWidth } from \"./pptx-viewer-viewport\";\nimport {\n  createPptxSlideLayout,\n  getPptxSlideAtScrollMarker,\n  getPptxSlideTop,\n  PPTX_READING_MARKER_RATIO,\n  usePptxVisibleSlide,\n} from \"./pptx-viewer-visible-slide\";\nimport { usePptxZoom } from \"./pptx-viewer-zoom\";\nimport {\n  createPptxZoomMotionController,\n  PPTX_ZOOM_MOTION_TOTAL_MS,\n} from \"./pptx-viewer-zoom-motion\";\nimport { useIsClient } from \"./use-is-client\";\nimport { ViewerControls } from \"./viewer-controls\";\nimport { ViewerErrorBoundary } from \"./viewer-error\";\n\nexport type { PptxDocumentSource, PptxViewerProps } from \"./pptx-viewer-types\";\nexport type {\n  PptxSourceLoadTiming,\n  PptxSlideRenderTiming,\n  PptxSlideOverlayProps,\n} from \"./pptx-viewer-core\";\n\nexport function preloadPptxViewer() {\n  preloadPptxRenderer();\n}\n\nexport type PptxResourceContentProps = Omit<PptxViewerProps, \"source\"> & {\n  resource: ViewerResource;\n};\n\nexport function PptxViewer(props: PptxViewerProps) {\n  const { source, ...resourceProps } = props;\n  const resource = React.useMemo(() => createViewerResource(source), [source]);\n  return <PptxResourceContent {...resourceProps} resource={resource} />;\n}\n\nexport function PptxResourceContent(props: PptxResourceContentProps) {\n  const isClient = useIsClient();\n  const resource = props.resource;\n\n  if (!isClient) {\n    return (\n      <PptxViewerFallback\n        className={props.className}\n        bare={props.bare}\n        fallbackSlideSize={props.fallbackSlideSize}\n        controls={props.controls}\n      />\n    );\n  }\n  return (\n    <ViewerErrorBoundary\n      className={props.className}\n      bare={props.bare}\n      download={\n        props.controls === false || props.download === false\n          ? null\n          : resource.originalDownload\n      }\n      format=\"pptx\"\n      resetKey={getPptxResetKey({\n        resourceKey: resource.keys.resource,\n        scale: props.scale,\n        defaultScale: props.defaultScale,\n        eager: props.eager ?? false,\n      })}\n      sourceKind={resource.sourceKind}\n      onRetry={() => evictPptxSource(resource.content)}\n    >\n      <React.Suspense\n        fallback={\n          <PptxViewerFallback\n            className={props.className}\n            bare={props.bare}\n            fallbackSlideSize={props.fallbackSlideSize}\n            controls={props.controls}\n          />\n        }\n      >\n        <PptxViewerContent\n          key={resource.keys.load}\n          {...props}\n          resource={resource}\n        />\n      </React.Suspense>\n    </ViewerErrorBoundary>\n  );\n}\n\nfunction PptxViewerContent({\n  resource,\n  className,\n  scale: controlledScale,\n  defaultScale,\n  download = true,\n  onScaleChange,\n  controls = true,\n  renderSlideOverlay,\n  onSlideRenderTiming,\n  onSourceLoadTiming,\n  onVisibleSlideChange,\n  onScrollProgressChange,\n  bare = false,\n  eager = false,\n}: Omit<PptxViewerProps, \"source\"> & { resource: ViewerResource }) {\n  const source = useRetainedPptxSource(resource.content, onSourceLoadTiming);\n  const downloadAction = download ? resource.originalDownload : null;\n\n  const [rotation, setRotation] = React.useState(0);\n  const scrollActivity = React.useMemo(() => createPptxScrollActivity(), []);\n  const { registerDocumentSurface, usesShellGeometry } =\n    useOptionalFileViewerRendererEnvironment();\n  const { containerRef, viewportWidth } = usePptxViewportWidth({\n    enabled: !usesShellGeometry,\n  });\n  const rendererFrame = useOptionalFileViewerRendererFrame({\n    fallbackInlineSize: viewportWidth,\n  });\n  const layoutInlineSize = resolveFileViewerRendererLayoutInlineSize({\n    fallbackInlineSize: viewportWidth,\n    rendererFrame,\n  });\n  const fitScale = getPptxFitScale(layoutInlineSize, source.baseSize.width);\n  const { isFitWidth, scaleControlsDisabled, setViewerScale, zoomScale } =\n    usePptxZoom({\n      controlledScale,\n      defaultScale,\n      fitScale,\n      onScaleChange,\n    });\n  const slideLayout = React.useMemo(\n    () =>\n      createPptxSlideLayout({\n        baseSize: source.baseSize,\n        zoomScale,\n        rotation,\n        slideCount: source.slideCount,\n        slideGap: PPTX_SLIDE_GAP,\n        slidePadding: PPTX_SLIDE_PADDING,\n      }),\n    [source.baseSize, source.slideCount, zoomScale, rotation],\n  );\n  const zoomMotion = React.useMemo(\n    () => createPptxZoomMotionController(slideLayout),\n    [slideLayout],\n  );\n  const {\n    captureZoomIntent,\n    currentSlide,\n    getScrollMetrics,\n    handleScroll,\n    scrollViewportRef,\n  } = usePptxVisibleSlide({\n    layout: slideLayout,\n    onScrollProgressChange,\n    onVisibleSlideChange,\n    zoomMotion,\n  });\n  const isDocumentTransitioning = rendererFrame.phase !== \"idle\";\n  // Toolbar zoom steps re-anchor the viewport center and relax a FLIP over\n  // the commit (pptx-viewer-zoom-motion). The sequence must flip in the zoom\n  // gesture's own render so the visual clip is already released when the\n  // enlarged opening frame paints; rapid steps re-arm the release timer.\n  const [zoomMotionSequence, setZoomMotionSequence] = React.useState(0);\n  const isZoomTransitioning = zoomMotionSequence > 0;\n  useKeyedMountEffect(joinEffectKey([zoomMotionSequence]), () => {\n    if (zoomMotionSequence === 0) return;\n    const timeout = setTimeout(\n      () => setZoomMotionSequence(0),\n      PPTX_ZOOM_MOTION_TOTAL_MS,\n    );\n    return () => clearTimeout(timeout);\n  });\n  const beginZoomMotion = React.useCallback(() => {\n    // A zoom step mid shell-slide keeps the shell's own anchor solve in\n    // charge; the centered relax only owns quiet-state zooms.\n    if (isDocumentTransitioning) return;\n    captureZoomIntent();\n    setZoomMotionSequence((sequence) => sequence + 1);\n  }, [captureZoomIntent, isDocumentTransitioning]);\n  // Preserve the reading position when the slide surface re-fits to a new width\n  // (the sidebar toggle). The fit-driven zoom scale is the layout key. A\n  // toolbar zoom step must NOT take this path: its layout commit already\n  // restored the viewport-CENTER anchor (pptx-viewer-zoom-motion), and the\n  // fraction restore would overwrite that scroll in the same commit. The\n  // sequence flips in the zoom gesture's own render, so the zoom's re-fit\n  // lands with the rebase disabled while the key still advances.\n  const { captureReadingFraction } = useReadingFractionRebase({\n    scrollerRef: scrollViewportRef,\n    layoutKey: zoomScale,\n    enabled: usesShellGeometry && !isZoomTransitioning,\n  });\n  const scrollInteractionRestoreRef = React.useRef<number | null>(null);\n  const scrollInteractionElementRef = React.useRef<HTMLElement | null>(null);\n  const documentSurfaceRef = React.useRef<HTMLDivElement | null>(null);\n  const [documentSurfaceElement, setDocumentSurfaceElementState] =\n    React.useState<HTMLDivElement | null>(null);\n  const resolveSurfaceMotionStyle =\n    React.useMemo<FileViewerDocumentSurfaceMotionResolver>(\n      () =>\n        createFileViewerFitWidthSurfaceMotionResolver({\n          // The slide surface centres with auto margins whatever the renderer\n          // frame's align is (a zoomed-out deck splits its leftover space\n          // evenly), so the margin model must say \"center\" too — modelling\n          // \"start\" over an mx-auto surface leaves the auto-margin\n          // re-centering uncompensated mid-slide.\n          align: \"center\",\n          direction: rendererFrame.direction,\n          isFitWidth,\n          stageInlineSize: slideLayout.slideWidth,\n        }),\n      [isFitWidth, rendererFrame.direction, slideLayout.slideWidth],\n    );\n  const preMotionAnchorRef = React.useRef<{\n    screenRelTop: number;\n    slideNumber: number;\n  } | null>(null);\n  const lastAnchorBlockRef = React.useRef<number | null>(null);\n  const writePptxAnchorBlockOffsetPx = React.useCallback(\n    (anchorBlock: number) => {\n      const element = documentSurfaceRef.current;\n      if (!element) return;\n      const safeAnchorBlock = Number.isFinite(anchorBlock) ? anchorBlock : 0;\n      lastAnchorBlockRef.current = safeAnchorBlock;\n      element.style.setProperty(\n        FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY,\n        `${safeAnchorBlock}px`,\n      );\n    },\n    [],\n  );\n  const writePptxDocumentAnchorBlockOffset = React.useCallback(() => {\n    const metrics = getScrollMetrics();\n    // Stage (physical) coordinates: the transform scales the stage, so the\n    // marker offset is anchored to the live DOM scroll position.\n    writePptxAnchorBlockOffsetPx(\n      Math.max(0, metrics.physicalScrollTop) +\n        Math.max(0, metrics.viewportHeight) * PPTX_READING_MARKER_RATIO,\n    );\n  }, [getScrollMetrics, writePptxAnchorBlockOffsetPx]);\n  // The transform must pin the exact screen line the slide-start commit\n  // preserved. Measured against the slide layout models (old model at\n  // capture, new model at solve), which is exact across the constant slide\n  // gap and padding, rebase clamps, and mid-flight retargets (the capture\n  // applies the in-flight transform it was seen under).\n  const writePptxMotionAnchorBlockOffset = React.useCallback(() => {\n    const metrics = getScrollMetrics();\n    // Slide offsets are logical; map through the paged-scroll delta into the\n    // stage's physical coordinates (the space the transform scales).\n    const logicalDelta = metrics.scrollTop - metrics.physicalScrollTop;\n    const preMotionAnchor = preMotionAnchorRef.current;\n    const anchorBlock = preMotionAnchor\n      ? resolveFileViewerFitWidthMotionAnchorBlock({\n          fromInlineSize: rendererFrame.fromInlineSize,\n          probeScreenOffset: preMotionAnchor.screenRelTop,\n          probeStageOffset:\n            getPptxSlideTop(slideLayout, preMotionAnchor.slideNumber - 1) -\n            logicalDelta,\n          scrollTop: metrics.physicalScrollTop,\n          stageInlineSize: slideLayout.slideWidth,\n          toInlineSize: rendererFrame.toInlineSize,\n        })\n      : null;\n\n    if (anchorBlock == null) {\n      writePptxDocumentAnchorBlockOffset();\n      return;\n    }\n    writePptxAnchorBlockOffsetPx(anchorBlock);\n  }, [\n    getScrollMetrics,\n    rendererFrame.fromInlineSize,\n    rendererFrame.toInlineSize,\n    slideLayout,\n    writePptxAnchorBlockOffsetPx,\n    writePptxDocumentAnchorBlockOffset,\n  ]);\n\n  const suspendScrollInteractions = React.useCallback(() => {\n    const scrollElement = scrollViewportRef.current?.querySelector<HTMLElement>(\n      '[data-slot=\"pptx-slide-sticky-window\"]',\n    );\n    if (!scrollElement) return;\n\n    if (scrollInteractionRestoreRef.current !== null) {\n      window.clearTimeout(scrollInteractionRestoreRef.current);\n    }\n    scrollInteractionElementRef.current = scrollElement;\n    scrollElement.style.pointerEvents = \"none\";\n    if (isMobileSafari()) {\n      scrollElement.style.overflowX = \"hidden\";\n    }\n    scrollInteractionRestoreRef.current = window.setTimeout(() => {\n      scrollInteractionRestoreRef.current = null;\n      restorePptxScrollInteractions(scrollInteractionElementRef.current);\n      scrollInteractionElementRef.current = null;\n    }, 120);\n  }, [scrollViewportRef]);\n\n  useMountEffect(() => () => {\n    if (scrollInteractionRestoreRef.current !== null) {\n      window.clearTimeout(scrollInteractionRestoreRef.current);\n      scrollInteractionRestoreRef.current = null;\n    }\n    restorePptxScrollInteractions(scrollInteractionElementRef.current);\n    scrollInteractionElementRef.current = null;\n  });\n\n  const handleViewportScroll = React.useCallback(() => {\n    captureReadingFraction();\n    scrollActivity.handleScroll();\n    suspendScrollInteractions();\n    handleScroll();\n  }, [\n    captureReadingFraction,\n    handleScroll,\n    scrollActivity,\n    suspendScrollInteractions,\n  ]);\n  // Held through the whole motion (see isDocumentTransitioning above): layout\n  // and scroll commit at slide start (commit-then-relax), but the union in\n  // the scroller still keeps the pre-motion slides mounted until the motion\n  // idles as re-render insurance.\n  const measureBeforeLayoutMotionRef = React.useRef(\n    (_liveFrame: FileViewerMotionFrame | null) => {},\n  );\n  measureBeforeLayoutMotionRef.current = (liveFrame) => {\n    const metrics = getScrollMetrics();\n    const logicalDelta = metrics.scrollTop - metrics.physicalScrollTop;\n    const slideNumber = getPptxSlideAtScrollMarker(\n      slideLayout,\n      Math.max(0, metrics.scrollTop) +\n        Math.max(0, metrics.viewportHeight) * PPTX_READING_MARKER_RATIO,\n    );\n    preMotionAnchorRef.current = {\n      screenRelTop: captureFileViewerFitWidthAnchorScreenOffset({\n        lastAnchorBlock: lastAnchorBlockRef.current,\n        liveFrame,\n        probeStageOffset:\n          getPptxSlideTop(slideLayout, slideNumber - 1) - logicalDelta,\n        scrollTop: metrics.physicalScrollTop,\n        stageInlineSize: slideLayout.slideWidth,\n      }),\n      slideNumber,\n    };\n    handleScroll();\n  };\n  const handleBeforeLayoutMotion = React.useCallback(\n    (event: Event) => {\n      captureReadingFraction();\n      measureBeforeLayoutMotionRef.current(\n        readFileViewerBeforeLayoutMotionFrame(event),\n      );\n    },\n    [captureReadingFraction],\n  );\n  // Runs inside the slide-start commit after the fraction rebase (hook order\n  // puts the rebase's layout effect first), pinning the transform before the\n  // first frame paints. Keyed on the transition id so a mid-flight retarget\n  // (same isTransitioning) re-solves against the new motion.\n  const isPptxShellTransitioning = rendererFrame.isTransitioning;\n  const writePptxMotionAnchorBlockOffsetRef = React.useRef(\n    writePptxMotionAnchorBlockOffset,\n  );\n  writePptxMotionAnchorBlockOffsetRef.current =\n    writePptxMotionAnchorBlockOffset;\n  useKeyedLayoutEffect(\n    joinEffectKey([\n      \"pptx-motion-anchor\",\n      isPptxShellTransitioning,\n      rendererFrame.documentTransition.transitionId,\n      writePptxMotionAnchorBlockOffset,\n    ]),\n    () => {\n      if (!isPptxShellTransitioning) return;\n      writePptxMotionAnchorBlockOffsetRef.current();\n    },\n  );\n  const setDocumentSurfaceElement = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      const previousElement = documentSurfaceRef.current;\n      if (previousElement === element) return;\n      previousElement?.removeEventListener(\n        FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,\n        handleBeforeLayoutMotion,\n      );\n      documentSurfaceRef.current = element;\n      setDocumentSurfaceElementState((previous) =>\n        previous === element ? previous : element,\n      );\n      if (!element) return;\n      element.addEventListener(\n        FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,\n        handleBeforeLayoutMotion,\n      );\n      writePptxDocumentAnchorBlockOffset();\n    },\n    [handleBeforeLayoutMotion, writePptxDocumentAnchorBlockOffset],\n  );\n  const documentSurfaceKey = documentSurfaceElement\n    ? joinEffectKey([\n        \"pptx-document-surface\",\n        documentSurfaceElement,\n        registerDocumentSurface,\n        resolveSurfaceMotionStyle,\n      ])\n    : null;\n  useKeyedLayoutEffect(documentSurfaceKey, () => {\n    if (!documentSurfaceElement) return;\n    return registerDocumentSurface({\n      element: documentSurfaceElement,\n      resolveMotionStyle: resolveSurfaceMotionStyle,\n    });\n  });\n\n  return (\n    <div\n      className={cn(\n        \"flex min-h-0 flex-col overflow-hidden\",\n        bare ? \"bg-muted/20 h-full\" : \"bg-muted/30 rounded-xl border\",\n        className,\n      )}\n      data-slot=\"pptx-viewer\"\n    >\n      {controls ? (\n        <ViewerControls\n          position={{\n            kind: \"slide\",\n            current: currentSlide,\n            total: source.slideCount,\n          }}\n          zoom={{\n            scale: zoomScale,\n            onZoomOut: () => {\n              beginZoomMotion();\n              setViewerScale(zoomScale / 1.2);\n            },\n            onZoomIn: () => {\n              beginZoomMotion();\n              setViewerScale(zoomScale * 1.2);\n            },\n            onFit: () => {\n              beginZoomMotion();\n              setViewerScale(null);\n            },\n            isDisabled: scaleControlsDisabled,\n          }}\n          rotate={{\n            onRotate: () => setRotation((value) => (value + 90) % 360),\n          }}\n          downloads={downloadAction ? [downloadAction] : []}\n        />\n      ) : null}\n\n      <div className=\"flex min-h-0 flex-1\">\n        <div className=\"relative flex min-h-0 min-w-0 flex-1 flex-col\">\n          <div className=\"relative flex min-h-0 flex-1 flex-col\">\n            <PptxSlideScroller\n              source={source}\n              zoomScale={zoomScale}\n              rotation={rotation}\n              layout={slideLayout}\n              eager={eager}\n              activity={scrollActivity}\n              renderSlideOverlay={renderSlideOverlay}\n              onSlideRenderTiming={onSlideRenderTiming}\n              containerRef={containerRef}\n              documentSurfaceRef={setDocumentSurfaceElement}\n              viewportRef={scrollViewportRef}\n              getScrollMetrics={getScrollMetrics}\n              isFitWidth={isFitWidth}\n              isTransitioning={isDocumentTransitioning}\n              onScroll={handleViewportScroll}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction restorePptxScrollInteractions(element: HTMLElement | null) {\n  if (!element) return;\n  element.style.removeProperty(\"pointer-events\");\n  element.style.removeProperty(\"overflow-x\");\n}\n\nfunction isMobileSafari() {\n  if (typeof navigator === \"undefined\") return false;\n  const userAgent = navigator.userAgent;\n  return (\n    /Safari/i.test(userAgent) &&\n    /Mobile/i.test(userAgent) &&\n    !/CriOS|FxiOS|EdgiOS/i.test(userAgent)\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-error.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { RotateCcw } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { ViewerDownloadAction } from \"@/lib/viewer-download-actions\";\nimport {\n  toViewerErrorInfo,\n  type ViewerErrorContext,\n  type ViewerFormat,\n} from \"@/lib/viewer-errors\";\n\nimport { Button } from \"./button\";\nimport { ViewerDownloadButton } from \"./viewer-download\";\n\nexport interface ViewerErrorStateProps extends ViewerErrorContext {\n  error: unknown;\n  download?: ViewerDownloadAction | null;\n  className?: string;\n  bare?: boolean;\n  variant?: \"card\" | \"document\" | \"inline\";\n  onRetry?: () => void;\n}\n\nexport function ViewerErrorState({\n  error,\n  format,\n  sourceKind,\n  canDownload,\n  retry,\n  download,\n  className,\n  bare = false,\n  variant = \"card\",\n  onRetry,\n}: ViewerErrorStateProps) {\n  const info = toViewerErrorInfo(error, {\n    format,\n    sourceKind,\n    canDownload: canDownload ?? Boolean(download && !download.isDisabled),\n    retry,\n  });\n  const showRetry = info.isRetryable && onRetry;\n  const showDownload =\n    info.isDownloadUseful && download != null && !download.isDisabled;\n\n  return (\n    <div\n      className={cn(errorStateClassName({ bare, variant }), className)}\n      data-error-domain={info.domain}\n      data-error-format={info.format}\n      data-error-kind={info.kind}\n      data-error-message={info.message}\n      data-slot=\"viewer-error\"\n      role=\"alert\"\n    >\n      <p>{info.userMessage}</p>\n      {showRetry || showDownload ? (\n        <div className=\"flex items-center gap-2\">\n          {showRetry ? (\n            <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n              <RotateCcw className=\"mr-1.5 size-4\" />\n              Retry\n            </Button>\n          ) : null}\n          {showDownload ? (\n            <ViewerDownloadButton\n              action={download}\n              variant={showRetry ? \"ghost\" : \"outline\"}\n              size=\"sm\"\n              className=\"\"\n              showLabel\n            />\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ViewerErrorBoundaryProps extends ViewerErrorContext {\n  children: React.ReactNode;\n  resetKey?: unknown;\n  download?: ViewerDownloadAction | null;\n  className?: string;\n  bare?: boolean;\n  variant?: \"card\" | \"document\" | \"inline\";\n  mapError?: (error: unknown) => unknown;\n  onRetry?: (error: unknown) => void;\n  onCaughtError?: (error: unknown, errorInfo: React.ErrorInfo) => void;\n}\n\nexport class ViewerErrorBoundary extends React.Component<\n  ViewerErrorBoundaryProps,\n  { error: unknown | null; retryKey: number }\n> {\n  state: Readonly<{ error: unknown | null; retryKey: number }> = {\n    error: null,\n    retryKey: 0,\n  };\n\n  componentDidUpdate(previousProps: ViewerErrorBoundaryProps) {\n    if (\n      previousProps.resetKey !== this.props.resetKey &&\n      this.state.error != null\n    ) {\n      this.setState({ error: null });\n    }\n  }\n\n  static getDerivedStateFromError(error: unknown) {\n    return { error };\n  }\n\n  componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {\n    this.props.onCaughtError?.(error, errorInfo);\n  }\n\n  render() {\n    if (this.state.error != null) {\n      const error = this.props.mapError\n        ? this.props.mapError(this.state.error)\n        : this.state.error;\n\n      return (\n        <ViewerErrorState\n          error={error}\n          format={this.props.format}\n          sourceKind={this.props.sourceKind}\n          canDownload={this.props.canDownload}\n          retry={this.props.retry}\n          download={this.props.download}\n          className={this.props.className}\n          bare={this.props.bare}\n          variant={this.props.variant}\n          onRetry={() => {\n            this.setState((state) => ({\n              error: null,\n              retryKey: state.retryKey + 1,\n            }));\n            this.props.onRetry?.(this.state.error);\n          }}\n        />\n      );\n    }\n\n    return (\n      <React.Fragment key={this.state.retryKey}>\n        {this.props.children}\n      </React.Fragment>\n    );\n  }\n}\n\nfunction errorStateClassName({\n  bare,\n  variant,\n}: {\n  bare: boolean;\n  variant: \"card\" | \"document\" | \"inline\";\n}) {\n  if (variant === \"inline\") {\n    return \"flex h-24 flex-col items-center justify-center gap-3 px-3 text-center text-xs text-muted-foreground\";\n  }\n  return cn(\n    \"flex min-h-64 flex-col items-center justify-center gap-3 p-6 text-center text-sm text-muted-foreground\",\n    bare ? \"bg-muted/20\" : \"rounded-xl border bg-muted/30\",\n    variant === \"document\" && \"min-h-full\",\n  );\n}\n\nexport type { ViewerFormat };\n",
      "type": "registry:ui",
      "target": "@ui/viewer-error.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-types.ts",
      "content": "import type * as React from \"react\";\n\nimport type { BlobViewerSource, UrlViewerSource } from \"@/lib/viewer-source\";\n\nimport type {\n  PptxSlideOverlayProps,\n  PptxSlideRenderTiming,\n  PptxSourceLoadTiming,\n} from \"./pptx-viewer-core\";\n\nexport type PptxDocumentSource = UrlViewerSource | BlobViewerSource;\n\nexport interface PptxViewerProps {\n  /** Canonical presentation source. */\n  source: PptxDocumentSource;\n  className?: string;\n  /** Controlled scale. When omitted, the viewer owns zoom state. */\n  scale?: number;\n  /** Initial uncontrolled scale. When omitted, uncontrolled mode starts fit-width. */\n  defaultScale?: number;\n  /** Intrinsic slide size used to reserve the first slide while metadata loads. */\n  fallbackSlideSize?: { width: number; height: number };\n  /** Called by zoom controls. `null` means return to fit-width mode. */\n  onScaleChange?: (scale: number | null) => void;\n  controls?: boolean;\n  /** Show download actions in this viewer's controls/error state. */\n  download?: boolean;\n  /** Render absolutely-positioned overlays, such as bbox citations, on each slide. */\n  renderSlideOverlay?: (props: PptxSlideOverlayProps) => React.ReactNode;\n  /** Reports measured canvas render work for benchmark and profiling surfaces. */\n  onSlideRenderTiming?: (timing: PptxSlideRenderTiming) => void;\n  /** Reports measured presentation fetch/parse/load work for benchmark surfaces. */\n  onSourceLoadTiming?: (timing: PptxSourceLoadTiming) => void;\n  /** Fired with the 1-based slide nearest the top of the viewport as you scroll. */\n  onVisibleSlideChange?: (slide: number) => void;\n  /** Fired with scroll progress in [0, 1]. */\n  onScrollProgressChange?: (progress: number) => void;\n  /** Drop the outer border/rounded/background so the viewer fills its container. */\n  bare?: boolean;\n  /** Render slides as soon as they near the viewport, even mid-scroll. */\n  eager?: boolean;\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-types.ts"
    },
    {
      "path": "registry/new-york-v4/lib/viewer-source.ts",
      "content": "export type FileCategory =\n  | \"pdf\"\n  | \"docx\"\n  | \"xlsx\"\n  | \"pptx\"\n  | \"csv\"\n  | \"image\"\n  | \"markdown\"\n  | \"html\"\n  | \"email\"\n  | \"text\"\n  | \"unsupported\";\n\nexport type ViewerSource = UrlViewerSource | TextSource | BlobViewerSource;\n\nexport interface UrlViewerSource {\n  kind: \"url\";\n  url: string;\n  fileName?: string;\n  mimeType?: string;\n  downloadUrl?: string;\n  identityKey?: string;\n}\n\nexport interface TextSource {\n  kind: \"text\";\n  text: string;\n  fileName?: string;\n  mimeType?: string;\n  identityKey?: string;\n}\n\nexport interface BlobViewerSource {\n  kind: \"blob\";\n  blob: Blob;\n  identityKey: string;\n  fileName?: string;\n  mimeType?: string;\n  downloadUrl?: string;\n}\n\nexport interface ViewerDescriptor {\n  source: ViewerSource;\n  category: FileCategory;\n  identityKey: string;\n  displayName: string;\n  fileName: string;\n  mimeType?: string;\n}\n\nconst EXTENSION_CATEGORY: Record<string, FileCategory> = {\n  pdf: \"pdf\",\n  docx: \"docx\",\n  xlsx: \"xlsx\",\n  xls: \"xlsx\",\n  xlsm: \"xlsx\",\n  pptx: \"pptx\",\n  csv: \"csv\",\n  tsv: \"csv\",\n  png: \"image\",\n  jpg: \"image\",\n  jpeg: \"image\",\n  gif: \"image\",\n  webp: \"image\",\n  avif: \"image\",\n  bmp: \"image\",\n  svg: \"image\",\n  ico: \"image\",\n  tif: \"image\",\n  tiff: \"image\",\n  md: \"markdown\",\n  markdown: \"markdown\",\n  mdx: \"text\",\n  html: \"html\",\n  htm: \"html\",\n  eml: \"email\",\n  txt: \"text\",\n  text: \"text\",\n  log: \"text\",\n  json: \"text\",\n  jsonl: \"text\",\n  json5: \"text\",\n  ndjson: \"text\",\n  xml: \"text\",\n  yaml: \"text\",\n  yml: \"text\",\n  toml: \"text\",\n  ini: \"text\",\n  env: \"text\",\n  js: \"text\",\n  mjs: \"text\",\n  cjs: \"text\",\n  jsx: \"text\",\n  ts: \"text\",\n  tsx: \"text\",\n  css: \"text\",\n  scss: \"text\",\n  less: \"text\",\n  py: \"text\",\n  rb: \"text\",\n  go: \"text\",\n  rs: \"text\",\n  java: \"text\",\n  kt: \"text\",\n  c: \"text\",\n  h: \"text\",\n  cpp: \"text\",\n  cc: \"text\",\n  cs: \"text\",\n  php: \"text\",\n  sh: \"text\",\n  bash: \"text\",\n  zsh: \"text\",\n  sql: \"text\",\n  graphql: \"text\",\n  proto: \"text\",\n  lua: \"text\",\n  r: \"text\",\n  swift: \"text\",\n  scala: \"text\",\n  pl: \"text\",\n  vue: \"text\",\n  svelte: \"text\",\n};\n\nexport function extensionOf(name: string): string | null {\n  const clean = name.split(/[?#]/)[0];\n  const base = clean.split(\"/\").pop() ?? clean;\n  const dot = base.lastIndexOf(\".\");\n  return dot > 0 ? base.slice(dot + 1).toLowerCase() : null;\n}\n\nexport function extractName(url: string): string {\n  const clean = url.split(/[?#]/)[0];\n  return clean.split(\"/\").pop() || \"file\";\n}\n\nexport function detectCategory(\n  fileName: string,\n  mimeType?: string,\n): FileCategory {\n  const ext = extensionOf(fileName);\n  if (ext && EXTENSION_CATEGORY[ext]) return EXTENSION_CATEGORY[ext];\n  if (mimeType) {\n    const fromMime = categoryFromMime(mimeType);\n    if (fromMime) return fromMime;\n  }\n  return \"unsupported\";\n}\n\nexport function resolveViewerDescriptor({\n  source,\n  category,\n}: {\n  source: ViewerSource;\n  category?: FileCategory;\n}): ViewerDescriptor {\n  const resolvedMimeType =\n    source.mimeType ??\n    (source.kind === \"blob\" && source.blob.type ? source.blob.type : undefined);\n  const displayName = source.fileName ?? defaultDisplayName(source);\n  const fileName = source.fileName ?? defaultFileName(source);\n  const resolvedCategory =\n    category ?? detectCategory(displayName, resolvedMimeType);\n\n  return {\n    source,\n    category: resolvedCategory,\n    identityKey: source.identityKey ?? defaultIdentityKey(source),\n    displayName,\n    fileName,\n    mimeType: resolvedMimeType,\n  };\n}\n\nfunction categoryFromMime(mimeType: string): FileCategory | null {\n  const mime = mimeType.toLowerCase().split(\";\")[0].trim();\n  if (mime === \"application/pdf\") return \"pdf\";\n  if (mime.includes(\"wordprocessingml\")) return \"docx\";\n  if (mime.includes(\"spreadsheet\") || mime.includes(\"ms-excel\")) return \"xlsx\";\n  if (mime.includes(\"presentation\") || mime.includes(\"ms-powerpoint\")) {\n    return \"pptx\";\n  }\n  if (mime === \"text/csv\" || mime === \"text/tab-separated-values\") return \"csv\";\n  if (mime === \"text/markdown\") return \"markdown\";\n  if (mime === \"text/html\") return \"html\";\n  if (mime === \"message/rfc822\" || mime === \"message/global\") {\n    return \"email\";\n  }\n  if (mime.startsWith(\"image/\")) return \"image\";\n  if (mime === \"application/json\" || mime === \"application/xml\") return \"text\";\n  if (mime.startsWith(\"text/\")) return \"text\";\n  return null;\n}\n\nfunction defaultDisplayName(source: ViewerSource) {\n  if (source.kind === \"url\") return source.url;\n  if (source.kind === \"text\") return \"text.txt\";\n  return \"file\";\n}\n\nfunction defaultFileName(source: ViewerSource) {\n  if (source.kind === \"url\") return extractName(source.url);\n  if (source.kind === \"text\") return \"text.txt\";\n  return \"file\";\n}\n\nfunction defaultIdentityKey(source: ViewerSource) {\n  if (source.kind === \"url\") return `url:${source.url}`;\n  if (source.kind === \"text\") return textPayloadIdentityKey(source.text);\n  return source.identityKey;\n}\n\nexport function textPayloadIdentityKey(text: string) {\n  return textPayloadKey(text);\n}\n\nexport function textPayloadKey(text: string) {\n  return `text:${text.length}:${hashString(text)}`;\n}\n\nfunction hashString(text: string) {\n  let hash = 0x811c9dc5;\n  for (let index = 0; index < text.length; index += 1) {\n    hash ^= text.charCodeAt(index);\n    hash = Math.imul(hash, 0x01000193);\n  }\n  return (hash >>> 0).toString(36);\n}\n",
      "type": "registry:lib",
      "target": "@lib/viewer-source.ts"
    },
    {
      "path": "registry/new-york-v4/lib/viewer-resource.ts",
      "content": "import {\n  createBlobDownloadAction,\n  createHrefDownloadAction,\n  createTextDownloadAction,\n  type ViewerDownloadAction,\n} from \"@/lib/viewer-download-actions\";\nimport {\n  isAbortError,\n  ResourceError,\n  type ResourceTooLargeReason,\n} from \"@/lib/viewer-errors\";\nimport {\n  resolveViewerDescriptor,\n  textPayloadKey,\n  type BlobViewerSource,\n  type FileCategory,\n  type TextSource,\n  type UrlViewerSource,\n  type ViewerDescriptor,\n  type ViewerSource,\n} from \"@/lib/viewer-source\";\n\nexport interface ResourceReadOptions {\n  cache?: RequestCache;\n  signal?: AbortSignal;\n}\n\nexport interface TextReadOptions extends ResourceReadOptions {\n  maxBytes?: number;\n  maxLines?: number;\n}\n\nexport interface ByteRange {\n  start: number;\n  end: number;\n}\n\nexport interface ByteRangeResult {\n  buffer: ArrayBuffer;\n  contentRange?: {\n    start: number;\n    end: number;\n    total: number | null;\n  };\n  isComplete: boolean;\n}\n\nexport interface ViewerResourceKeys {\n  readonly load: string;\n  readonly presentation: string;\n  readonly resource: string;\n}\n\nexport type ViewerResourcePayload =\n  | { kind: \"url\"; url: string }\n  | { kind: \"blob\"; blob: Blob }\n  | { kind: \"text\"; text: string };\n\nexport interface ViewerResourceContent {\n  readonly key: string;\n  readonly sourceKind: ViewerSource[\"kind\"];\n  readonly directUrl: string | null;\n  readonly mimeType?: string;\n  readonly payload: ViewerResourcePayload;\n  readBlob(options?: ResourceReadOptions): Promise<Blob>;\n  readBytes(options?: ResourceReadOptions): Promise<ArrayBuffer>;\n  readText(options?: TextReadOptions): Promise<string>;\n  readStream(\n    options?: ResourceReadOptions,\n  ): Promise<ReadableStream<Uint8Array>>;\n  readRange(\n    range: ByteRange,\n    options?: ResourceReadOptions,\n  ): Promise<ByteRangeResult>;\n}\n\nexport type ViewerContentIdentity = Pick<\n  ViewerResourceContent,\n  \"key\" | \"sourceKind\"\n>;\n\nexport type ViewerContentDirectUrl = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"directUrl\">;\n\nexport type ViewerContentPayload = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"payload\">;\n\nexport type ViewerContentMime = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"mimeType\">;\n\nexport type ViewerContentBlob = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"readBlob\">;\n\nexport type ViewerContentBytes = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"readBytes\">;\n\nexport type ViewerContentText = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"readText\">;\n\nexport type ViewerContentStream = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"readStream\">;\n\nexport type ViewerContentRange = ViewerContentIdentity &\n  Pick<ViewerResourceContent, \"readRange\">;\n\nexport interface ViewerResource {\n  readonly descriptor: ViewerDescriptor;\n  readonly sourceKind: ViewerSource[\"kind\"];\n  readonly keys: ViewerResourceKeys;\n  readonly identityKey: string;\n  readonly fileName: string;\n  readonly mimeType?: string;\n  readonly content: ViewerResourceContent;\n  readonly originalDownload: ViewerDownloadAction;\n}\n\nconst URL_RESOURCE_REGISTRY_MAX = 128;\nconst TEXT_RESOURCE_REGISTRY_MAX = 64;\n// LF, CR, CRLF, LINE SEPARATOR (U+2028), and PARAGRAPH SEPARATOR (U+2029) — the\n// ECMAScript LineTerminator set, matching what a browser breaks on in a\n// `white-space: pre` block. Kept in sync with text-viewer-resource's splitter.\nconst TEXT_LINE_BREAK_PATTERN = /\\r\\n|[\\n\\r\\u2028\\u2029]/g;\n\nconst urlViewerResourceRegistry = new Map<string, ViewerResource>();\nconst urlViewerResourceContentRegistry = new Map<\n  string,\n  ViewerResourceContent\n>();\nconst textViewerResourceRegistry = new Map<string, ViewerResource>();\nconst textViewerResourceContentRegistry = new Map<\n  string,\n  ViewerResourceContent\n>();\nlet blobViewerResourceRegistry = new WeakMap<\n  Blob,\n  Map<string, ViewerResource>\n>();\nlet blobViewerResourceContentRegistry = new WeakMap<\n  Blob,\n  Map<string, ViewerResourceContent>\n>();\nconst blobObjectKeys = new WeakMap<Blob, string>();\nlet nextBlobObjectKey = 0;\n\nexport function createViewerResource(\n  source: ViewerSource,\n  category?: FileCategory,\n): ViewerResource {\n  const descriptor = resolveViewerDescriptor({ source, category });\n  const keys = viewerResourceKeys(source, descriptor);\n\n  if (source.kind === \"url\") {\n    return internUrlResource(source, descriptor, keys);\n  }\n  if (source.kind === \"blob\") {\n    return internBlobResource(source, descriptor, keys);\n  }\n  return internTextResource(source, descriptor, keys);\n}\n\nexport function clearViewerResourceRegistryForTests() {\n  urlViewerResourceRegistry.clear();\n  urlViewerResourceContentRegistry.clear();\n  textViewerResourceRegistry.clear();\n  textViewerResourceContentRegistry.clear();\n  blobViewerResourceRegistry = new WeakMap();\n  blobViewerResourceContentRegistry = new WeakMap();\n}\n\nfunction internUrlResource(\n  source: UrlViewerSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n): ViewerResource {\n  const cached = urlViewerResourceRegistry.get(keys.resource);\n  if (cached) return cached;\n\n  const resource = createUrlResource(source, descriptor, keys);\n  urlViewerResourceRegistry.set(keys.resource, resource);\n  pruneUrlResourceRegistry();\n  return resource;\n}\n\nfunction internBlobResource(\n  source: BlobViewerSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n): ViewerResource {\n  let resources = blobViewerResourceRegistry.get(source.blob);\n  if (!resources) {\n    resources = new Map();\n    blobViewerResourceRegistry.set(source.blob, resources);\n  }\n\n  const cached = resources.get(keys.resource);\n  if (cached) return cached;\n\n  const resource = createBlobResource(source, descriptor, keys);\n  resources.set(keys.resource, resource);\n  return resource;\n}\n\nfunction internTextResource(\n  source: TextSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n): ViewerResource {\n  const cached = textViewerResourceRegistry.get(keys.resource);\n  if (cached) return cached;\n\n  const resource = createTextResource(source, descriptor, keys);\n  textViewerResourceRegistry.set(keys.resource, resource);\n  pruneTextResourceRegistry();\n  return resource;\n}\n\nfunction pruneUrlResourceRegistry() {\n  while (urlViewerResourceRegistry.size > URL_RESOURCE_REGISTRY_MAX) {\n    const firstKey = urlViewerResourceRegistry.keys().next().value;\n    if (!firstKey) return;\n    urlViewerResourceRegistry.delete(firstKey);\n  }\n}\n\nfunction pruneUrlResourceContentRegistry() {\n  while (urlViewerResourceContentRegistry.size > URL_RESOURCE_REGISTRY_MAX) {\n    const firstKey = urlViewerResourceContentRegistry.keys().next().value;\n    if (!firstKey) return;\n    urlViewerResourceContentRegistry.delete(firstKey);\n  }\n}\n\nfunction pruneTextResourceRegistry() {\n  while (textViewerResourceRegistry.size > TEXT_RESOURCE_REGISTRY_MAX) {\n    const firstKey = textViewerResourceRegistry.keys().next().value;\n    if (!firstKey) return;\n    textViewerResourceRegistry.delete(firstKey);\n  }\n}\n\nfunction pruneTextResourceContentRegistry() {\n  while (textViewerResourceContentRegistry.size > TEXT_RESOURCE_REGISTRY_MAX) {\n    const firstKey = textViewerResourceContentRegistry.keys().next().value;\n    if (!firstKey) return;\n    textViewerResourceContentRegistry.delete(firstKey);\n  }\n}\n\nfunction viewerResourceKeys(\n  source: ViewerSource,\n  descriptor: ViewerDescriptor,\n): ViewerResourceKeys {\n  const load = viewerResourceLoadKey(source, descriptor);\n  const presentation = viewerResourcePresentationKey(source, descriptor);\n  return {\n    load,\n    presentation,\n    resource: [load, presentation].join(\"\\u0000\"),\n  };\n}\n\nfunction viewerResourceLoadKey(\n  source: ViewerSource,\n  descriptor: ViewerDescriptor,\n) {\n  return [\n    source.kind,\n    source.identityKey ?? \"\",\n    sourceMimeType(source) ?? \"\",\n    directLoadCacheKey(source),\n    payloadCacheKey(source, descriptor),\n  ].join(\"\\u0000\");\n}\n\nfunction viewerResourcePresentationKey(\n  source: ViewerSource,\n  descriptor: ViewerDescriptor,\n) {\n  return [\n    descriptor.category,\n    descriptor.displayName,\n    descriptor.fileName,\n    descriptor.mimeType ?? \"\",\n    downloadCacheKey(source),\n  ].join(\"\\u0000\");\n}\n\nfunction directLoadCacheKey(source: ViewerSource) {\n  return source.kind === \"url\" ? source.url : \"\";\n}\n\nfunction downloadCacheKey(source: ViewerSource) {\n  if (source.kind === \"text\") return \"\";\n  return source.downloadUrl ?? \"\";\n}\n\nfunction payloadCacheKey(source: ViewerSource, descriptor: ViewerDescriptor) {\n  if (source.kind === \"url\") return \"\";\n  if (source.kind === \"blob\") return blobObjectKey(source.blob);\n  return source.identityKey ? \"\" : descriptor.identityKey;\n}\n\nexport function viewerResourceRenderKey(resource: ViewerResource): string {\n  const load = [\n    resource.sourceKind,\n    resource.identityKey,\n    resource.mimeType ?? resource.content.mimeType ?? \"\",\n    resource.content.directUrl ?? \"\",\n    viewerContentRenderKey(resource.content),\n  ].join(\"\\u0000\");\n\n  return [load, resource.keys.presentation].join(\"\\u0000\");\n}\n\nexport function viewerContentRenderKey(content: ViewerResourceContent): string {\n  if (content.payload.kind === \"text\")\n    return textPayloadKey(content.payload.text);\n  return content.key;\n}\n\nfunction sourceMimeType(source: ViewerSource) {\n  if (source.kind === \"blob\") return source.mimeType ?? source.blob.type;\n  return source.mimeType;\n}\n\nfunction blobObjectKey(blob: Blob) {\n  let key = blobObjectKeys.get(blob);\n  if (!key) {\n    nextBlobObjectKey += 1;\n    key = `blob-object:${nextBlobObjectKey}`;\n    blobObjectKeys.set(blob, key);\n  }\n  return key;\n}\n\nexport function blobSource(\n  bytes: Blob | ArrayBuffer | Uint8Array,\n  metadata: {\n    identityKey: string;\n    fileName?: string;\n    mimeType?: string;\n    downloadUrl?: string;\n  },\n): BlobViewerSource {\n  const blob =\n    bytes instanceof Blob\n      ? bytes\n      : new Blob(\n          [bytes instanceof ArrayBuffer ? bytes : new Uint8Array(bytes)],\n          {\n            type: metadata.mimeType ?? \"\",\n          },\n        );\n  return {\n    kind: \"blob\",\n    blob,\n    fileName: metadata.fileName,\n    mimeType: metadata.mimeType ?? blob.type,\n    downloadUrl: metadata.downloadUrl,\n    identityKey: metadata.identityKey,\n  };\n}\n\nfunction createUrlResource(\n  source: UrlViewerSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n): ViewerResource {\n  const content = internUrlResourceContent(source, keys);\n  const originalDownload = createHrefDownloadAction({\n    id: \"download-original\",\n    label: \"Download\",\n    href: source.downloadUrl ?? source.url,\n    fileName: descriptor.fileName,\n  });\n\n  return resourceBase(source, descriptor, keys, { content, originalDownload });\n}\n\nfunction internUrlResourceContent(\n  source: UrlViewerSource,\n  keys: ViewerResourceKeys,\n): ViewerResourceContent {\n  const cached = urlViewerResourceContentRegistry.get(keys.load);\n  if (cached) return cached;\n\n  const content = resourceContentBase(source, keys, {\n    directUrl: source.url,\n    payload: { kind: \"url\", url: source.url },\n    readBlob: async ({ cache, signal } = {}) => {\n      const response = await fetchResource(\n        source.url,\n        cache ? { cache, signal } : { signal },\n      );\n      validateFullContentResponse(response);\n      return readResponseBlob(response);\n    },\n    readBytes: async ({ cache, signal } = {}) => {\n      const response = await fetchResource(\n        source.url,\n        cache ? { cache, signal } : { signal },\n      );\n      validateFullContentResponse(response);\n      return readResponseArrayBuffer(response);\n    },\n    readText: async ({ cache, signal, maxBytes, maxLines } = {}) => {\n      const response = await fetchResource(\n        source.url,\n        cache ? { cache, signal } : { signal },\n      );\n      return readBoundedResponseText(response, { maxBytes, maxLines });\n    },\n    readStream: async ({ cache, signal } = {}) => {\n      const response = await fetchResource(\n        source.url,\n        cache ? { cache, signal } : { signal },\n      );\n      validateFullContentResponse(response);\n      if (!response.body) {\n        if (response.status === 204 || response.status === 205) {\n          return emptyByteStream();\n        }\n        throw new ResourceError({\n          kind: \"unsupported_capability\",\n          message: \"This response cannot be streamed.\",\n        });\n      }\n      return response.body;\n    },\n    readRange: async (range, { cache, signal } = {}) => {\n      validateByteRange(range);\n      const { start, end } = range;\n      const init = {\n        signal,\n        headers: { Range: `bytes=${start}-${end}` },\n      };\n      const response = await fetchResource(\n        source.url,\n        cache ? { ...init, cache } : init,\n      );\n      const buffer = await readResponseArrayBuffer(response);\n      const contentRange = parseContentRange(\n        response.headers.get(\"content-range\"),\n      );\n      validateUrlRangeResponse({\n        bufferLength: buffer.byteLength,\n        contentRange,\n        range,\n        status: response.status,\n      });\n      return {\n        buffer,\n        contentRange,\n        isComplete: isByteRangeComplete({\n          bufferLength: buffer.byteLength,\n          contentRange,\n          requestedLength: end - start + 1,\n          status: response.status,\n        }),\n      };\n    },\n  });\n  urlViewerResourceContentRegistry.set(keys.load, content);\n  pruneUrlResourceContentRegistry();\n  return content;\n}\n\nfunction emptyByteStream() {\n  return new ReadableStream<Uint8Array>({\n    start(controller) {\n      controller.close();\n    },\n  });\n}\n\nfunction createBlobResource(\n  source: BlobViewerSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n): ViewerResource {\n  const blob = source.blob;\n  const content = internBlobResourceContent(source, keys);\n  const originalDownload = source.downloadUrl\n    ? createHrefDownloadAction({\n        id: \"download-original\",\n        label: \"Download\",\n        href: source.downloadUrl,\n        fileName: descriptor.fileName,\n      })\n    : createBlobDownloadAction({\n        id: \"download-original\",\n        label: \"Download\",\n        blob,\n        fileName: descriptor.fileName,\n      });\n\n  return resourceBase(source, descriptor, keys, { content, originalDownload });\n}\n\nfunction internBlobResourceContent(\n  source: BlobViewerSource,\n  keys: ViewerResourceKeys,\n): ViewerResourceContent {\n  const blob = source.blob;\n  let contents = blobViewerResourceContentRegistry.get(blob);\n  if (!contents) {\n    contents = new Map();\n    blobViewerResourceContentRegistry.set(blob, contents);\n  }\n\n  const cached = contents.get(keys.load);\n  if (cached) return cached;\n\n  const content = resourceContentBase(source, keys, {\n    directUrl: null,\n    payload: { kind: \"blob\", blob },\n    readBlob: async () => blob,\n    readBytes: async () => blob.arrayBuffer(),\n    readText: async ({ maxBytes, maxLines } = {}) =>\n      readBoundedBlobText(blob, { maxBytes, maxLines }),\n    readStream: async () => blob.stream(),\n    readRange: async (range) => {\n      validateByteRange(range);\n      const { start, end } = range;\n      validateKnownByteRangeStart(start, blob.size);\n      const rangeBlob = blob.slice(start, end + 1);\n      return {\n        buffer: await rangeBlob.arrayBuffer(),\n        contentRange: {\n          start,\n          end: Math.min(end, blob.size - 1),\n          total: blob.size,\n        },\n        isComplete: end >= blob.size - 1,\n      };\n    },\n  });\n  contents.set(keys.load, content);\n  return content;\n}\n\nfunction createTextResource(\n  source: TextSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n): ViewerResource {\n  const content = internTextResourceContent(source, keys);\n  const originalDownload = createTextDownloadAction({\n    id: \"download-original\",\n    label: \"Download\",\n    text: source.text,\n    fileName: descriptor.fileName,\n    mimeType: descriptor.mimeType,\n  });\n\n  return resourceBase(source, descriptor, keys, { content, originalDownload });\n}\n\nfunction internTextResourceContent(\n  source: TextSource,\n  keys: ViewerResourceKeys,\n): ViewerResourceContent {\n  const cached = textViewerResourceContentRegistry.get(keys.load);\n  if (cached) return cached;\n\n  const content = resourceContentBase(source, keys, {\n    directUrl: null,\n    payload: { kind: \"text\", text: source.text },\n    readBlob: async () =>\n      new Blob([source.text], {\n        type: \"text/plain;charset=utf-8\",\n      }),\n    readBytes: async () =>\n      typedArrayBuffer(new TextEncoder().encode(source.text)),\n    readText: async ({ maxBytes, maxLines } = {}) =>\n      readBoundedInlineText(source.text, { maxBytes, maxLines }),\n    readStream: async () => new Blob([source.text]).stream(),\n    readRange: async (range) => {\n      validateByteRange(range);\n      const { start, end } = range;\n      const buffer = new TextEncoder().encode(source.text);\n      validateKnownByteRangeStart(start, buffer.byteLength);\n      const slice = buffer.slice(start, end + 1);\n      return {\n        buffer: typedArrayBuffer(slice),\n        contentRange: {\n          start,\n          end: Math.min(end, buffer.byteLength - 1),\n          total: buffer.byteLength,\n        },\n        isComplete: end >= buffer.byteLength - 1,\n      };\n    },\n  });\n  textViewerResourceContentRegistry.set(keys.load, content);\n  pruneTextResourceContentRegistry();\n  return content;\n}\n\nfunction resourceBase(\n  source: ViewerSource,\n  descriptor: ViewerDescriptor,\n  keys: ViewerResourceKeys,\n  options: {\n    content: ViewerResourceContent;\n    originalDownload: ViewerDownloadAction;\n  },\n): ViewerResource {\n  const { content, originalDownload } = options;\n  return Object.freeze({\n    descriptor,\n    sourceKind: source.kind,\n    keys,\n    identityKey: descriptor.identityKey,\n    fileName: descriptor.fileName,\n    mimeType: descriptor.mimeType,\n    content,\n    originalDownload,\n  });\n}\n\nfunction resourceContentBase(\n  source: ViewerSource,\n  keys: ViewerResourceKeys,\n  methods: Omit<ViewerResourceContent, \"key\" | \"sourceKind\" | \"mimeType\">,\n): ViewerResourceContent {\n  return Object.freeze({\n    key: keys.load,\n    sourceKind: source.kind,\n    mimeType: sourceMimeType(source),\n    ...methods,\n  });\n}\n\nfunction typedArrayBuffer(bytes: Uint8Array): ArrayBuffer {\n  return bytes.buffer.slice(\n    bytes.byteOffset,\n    bytes.byteOffset + bytes.byteLength,\n  ) as ArrayBuffer;\n}\n\nasync function fetchResource(\n  input: RequestInfo | URL,\n  init?: RequestInit,\n): Promise<Response> {\n  let response: Response;\n  try {\n    response = await fetch(input, init);\n  } catch (error) {\n    if (isAbortError(error)) {\n      throw new ResourceError({\n        kind: \"aborted\",\n        message: \"Loading was cancelled.\",\n        cause: error,\n      });\n    }\n    throw new ResourceError({\n      kind: \"fetch_failed\",\n      message: \"Could not fetch this resource.\",\n      cause: error,\n    });\n  }\n\n  if (!response.ok && response.status !== 206) {\n    throw new ResourceError({\n      kind: \"http_error\",\n      message: `Failed to load resource: ${response.status}`,\n      status: response.status,\n    });\n  }\n\n  return response;\n}\n\nasync function readBoundedResponseText(\n  response: Response,\n  bounds: { maxBytes?: number; maxLines?: number },\n) {\n  validateFullContentResponse(response);\n\n  const maxBytes = bounds.maxBytes;\n  if (\n    isContentLengthOverLimit(response.headers.get(\"content-length\"), maxBytes)\n  ) {\n    throw tooLarge(\"bytes\");\n  }\n\n  const body = response.body;\n  if (!body) {\n    const buffer = await readResponseArrayBuffer(response);\n    if (maxBytes != null && buffer.byteLength > maxBytes) {\n      throw tooLarge(\"bytes\");\n    }\n    const text = new TextDecoder().decode(buffer);\n    assertLineLimit(text, bounds.maxLines);\n    return text;\n  }\n\n  const reader = body.getReader();\n  const decoder = new TextDecoder();\n  const lineLimitTracker = createLineLimitTracker(bounds.maxLines);\n  let receivedBytes = 0;\n  let text = \"\";\n\n  while (true) {\n    const { done, value } = await readResponseStreamChunk(reader);\n    if (done) break;\n    receivedBytes += value.byteLength;\n    if (maxBytes != null && receivedBytes > maxBytes) {\n      await cancelReaderSilently(reader);\n      throw tooLarge(\"bytes\");\n    }\n    const chunkText = decoder.decode(value, { stream: true });\n    try {\n      lineLimitTracker.push(chunkText);\n    } catch (error) {\n      await cancelReaderSilently(reader);\n      throw error;\n    }\n    text += chunkText;\n  }\n\n  const finalText = decoder.decode();\n  lineLimitTracker.push(finalText);\n  text += finalText;\n  return text;\n}\n\nfunction isContentLengthOverLimit(\n  contentLength: string | null,\n  maxBytes: number | undefined,\n) {\n  if (maxBytes == null || contentLength == null) return false;\n\n  const normalizedLength = contentLength.trim().replace(/^0+(?=\\d)/, \"\");\n  if (!/^\\d+$/.test(normalizedLength)) return false;\n\n  const maxLength = String(maxBytes);\n  return (\n    normalizedLength.length > maxLength.length ||\n    (normalizedLength.length === maxLength.length &&\n      normalizedLength > maxLength)\n  );\n}\n\nfunction validateFullContentResponse(response: Response) {\n  if (response.status !== 206) return;\n\n  const contentRange = parseContentRange(response.headers.get(\"content-range\"));\n  if (\n    contentRange?.total != null &&\n    contentRange.start === 0 &&\n    contentRange.end === contentRange.total - 1\n  ) {\n    return;\n  }\n\n  throw new ResourceError({\n    kind: \"partial_content\",\n    message: \"Full response returned partial content.\",\n    status: response.status,\n  });\n}\n\nasync function readResponseStreamChunk(\n  reader: ReadableStreamDefaultReader<Uint8Array>,\n) {\n  try {\n    return await reader.read();\n  } catch (error) {\n    throw resourceReadError(error);\n  }\n}\n\nasync function readResponseArrayBuffer(response: Response) {\n  try {\n    return await response.arrayBuffer();\n  } catch (error) {\n    throw resourceReadError(error);\n  }\n}\n\nasync function readResponseBlob(response: Response) {\n  try {\n    return await response.blob();\n  } catch (error) {\n    throw resourceReadError(error);\n  }\n}\n\nfunction resourceReadError(error: unknown) {\n  if (isAbortError(error)) {\n    return new ResourceError({\n      kind: \"aborted\",\n      message: \"Loading was cancelled.\",\n      cause: error,\n    });\n  }\n  return new ResourceError({\n    kind: \"fetch_failed\",\n    message: \"Could not read this resource.\",\n    cause: error,\n  });\n}\n\nasync function readBoundedBlobText(\n  blob: Blob,\n  bounds: { maxBytes?: number; maxLines?: number },\n) {\n  if (bounds.maxBytes != null && blob.size > bounds.maxBytes) {\n    throw tooLarge(\"bytes\");\n  }\n  const text = await blob.text();\n  assertLineLimit(text, bounds.maxLines);\n  return text;\n}\n\nfunction readBoundedInlineText(\n  text: string,\n  { maxBytes, maxLines }: { maxBytes?: number; maxLines?: number },\n) {\n  // For inline sources the string *is* the resource, so its UTF-8 byte length\n  // is the authoritative size to measure against maxBytes.\n  if (\n    maxBytes != null &&\n    new TextEncoder().encode(text).byteLength > maxBytes\n  ) {\n    throw tooLarge(\"bytes\");\n  }\n  assertLineLimit(text, maxLines);\n  return text;\n}\n\n// Used after a transferred-byte check has already enforced maxBytes (URL/blob).\n// Re-encoding the decoded text here would double-count: invalid UTF-8 decodes to\n// U+FFFD (3 bytes each), inflating the measured size past the real wire bytes\n// and falsely rejecting small resources as \"too large\".\nfunction assertLineLimit(text: string, maxLines: number | undefined) {\n  if (\n    maxLines != null &&\n    text.split(TEXT_LINE_BREAK_PATTERN).length > maxLines\n  ) {\n    throw tooLarge(\"lines\");\n  }\n}\n\nfunction tooLarge(reason: ResourceTooLargeReason) {\n  return new ResourceError({\n    kind: \"too_large\",\n    tooLargeReason: reason,\n    message: `Resource exceeds ${reason} limit.`,\n  });\n}\n\nasync function cancelReaderSilently(\n  reader: ReadableStreamDefaultReader<Uint8Array>,\n) {\n  try {\n    await reader.cancel();\n  } catch {\n    // Preserve the user-facing load failure; cancellation is best-effort cleanup.\n  }\n}\n\nfunction validateByteRange({ start, end }: ByteRange) {\n  if (\n    !Number.isSafeInteger(start) ||\n    !Number.isSafeInteger(end) ||\n    start < 0 ||\n    end < start\n  ) {\n    throw new ResourceError({\n      kind: \"invalid_range\",\n      message: \"Byte range must use non-negative integer bounds.\",\n    });\n  }\n}\n\nfunction validateKnownByteRangeStart(start: number, total: number) {\n  if (start > 0 && start >= total) {\n    throw new ResourceError({\n      kind: \"invalid_range\",\n      message: \"Byte range starts past the available resource.\",\n    });\n  }\n}\n\nfunction validateUrlRangeResponse({\n  bufferLength,\n  contentRange,\n  range,\n  status,\n}: {\n  bufferLength: number;\n  contentRange: ByteRangeResult[\"contentRange\"];\n  range: ByteRange;\n  status: number;\n}) {\n  if (status === 200) {\n    if (range.start !== 0 || bufferLength > range.end - range.start + 1) {\n      throw new ResourceError({\n        kind: \"invalid_range\",\n        message: \"Full response does not match the requested byte range.\",\n      });\n    }\n    return;\n  }\n  if (status !== 206) {\n    throw new ResourceError({\n      kind: \"invalid_range\",\n      message: \"Range response must return full or partial content.\",\n    });\n  }\n  if (!contentRange) {\n    throw new ResourceError({\n      kind: \"invalid_range\",\n      message: \"Partial content response is missing a valid byte range.\",\n    });\n  }\n  const declaredLength = contentRange.end - contentRange.start + 1;\n  if (\n    contentRange.start !== range.start ||\n    contentRange.end < contentRange.start ||\n    contentRange.end > range.end ||\n    (contentRange.total != null && contentRange.end >= contentRange.total) ||\n    declaredLength !== bufferLength\n  ) {\n    throw new ResourceError({\n      kind: \"invalid_range\",\n      message: \"Response byte range does not match the requested range.\",\n    });\n  }\n}\n\nfunction isByteRangeComplete({\n  bufferLength,\n  contentRange,\n  requestedLength,\n  status,\n}: {\n  bufferLength: number;\n  contentRange: ByteRangeResult[\"contentRange\"];\n  requestedLength: number;\n  status: number;\n}) {\n  if (status === 200) return true;\n  if (contentRange?.total != null) {\n    if (contentRange.total <= 0) return true;\n    return contentRange.end >= contentRange.total - 1;\n  }\n  if (contentRange) return false;\n  return bufferLength < requestedLength;\n}\n\nfunction isStandaloneLineBreak(character: string) {\n  const code = character.charCodeAt(0);\n  return code === 0x0a || code === 0x2028 || code === 0x2029;\n}\n\nfunction createLineLimitTracker(maxLines: number | undefined) {\n  let lineCount = 1;\n  let previousWasCR = false;\n\n  return {\n    push(text: string) {\n      if (maxLines == null || text.length === 0) return;\n\n      for (const character of text) {\n        if (previousWasCR) {\n          previousWasCR = false;\n          if (character === \"\\n\") continue;\n        }\n\n        if (character === \"\\r\") {\n          lineCount += 1;\n          previousWasCR = true;\n        } else if (isStandaloneLineBreak(character)) {\n          // LF, plus LINE/PARAGRAPH SEPARATOR (U+2028/U+2029); none pair with CR.\n          lineCount += 1;\n        }\n\n        if (lineCount > maxLines) {\n          throw tooLarge(\"lines\");\n        }\n      }\n    },\n  };\n}\n\nfunction parseContentRange(value: string | null) {\n  if (!value) return undefined;\n  const match = value.match(/^bytes\\s+(\\d+)-(\\d+)\\/(\\d+|\\*)\\s*$/i);\n  if (!match) return undefined;\n  const start = parseContentRangeNumber(match[1]);\n  const end = parseContentRangeNumber(match[2]);\n  const total =\n    match[3] === \"*\" ? null : parseContentRangeNumber(match[3] ?? \"\");\n  if (start == null || end == null || total === undefined) return undefined;\n  return {\n    start,\n    end,\n    total,\n  };\n}\n\nfunction parseContentRangeNumber(value: string) {\n  const number = Number(value);\n  return Number.isSafeInteger(number) ? number : undefined;\n}\n",
      "type": "registry:lib",
      "target": "@lib/viewer-resource.ts"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-download.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Download } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  ViewerDownloadError,\n  type ViewerDownloadAction,\n  type ViewerDownloadPayload,\n} from \"@/lib/viewer-download-actions\";\n\nimport { Spinner } from \"@/components/ui/spinner\";\n\nimport { Button, buttonVariants } from \"./button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport interface TriggerViewerDownloadOptions {\n  signal?: AbortSignal;\n}\n\nexport async function triggerViewerDownload(\n  action: ViewerDownloadAction,\n  options?: TriggerViewerDownloadOptions,\n): Promise<void> {\n  if (action.isDisabled) {\n    throw new ViewerDownloadError({\n      actionId: action.id,\n      kind: \"disabled\",\n      message: \"This download is disabled.\",\n    });\n  }\n\n  let payload: ViewerDownloadPayload;\n  try {\n    payload = await action.getPayload(options);\n  } catch (error) {\n    if (isAbortError(error)) {\n      throw new ViewerDownloadError({\n        actionId: action.id,\n        kind: \"aborted\",\n        message: \"Download was cancelled.\",\n        cause: error,\n      });\n    }\n    throw new ViewerDownloadError({\n      actionId: action.id,\n      kind: \"payload_failed\",\n      message: \"Could not prepare this download.\",\n      cause: error,\n    });\n  }\n\n  if (payload.kind === \"none\") return;\n  if (payload.kind === \"href\") {\n    try {\n      clickDownload(payload.href, action.fileName);\n    } catch (error) {\n      throw new ViewerDownloadError({\n        actionId: action.id,\n        kind: \"unsupported\",\n        message: \"Could not start this download.\",\n        cause: error,\n      });\n    }\n    return;\n  }\n\n  try {\n    const blob =\n      payload.kind === \"blob\"\n        ? payload.blob\n        : new Blob([payload.text], {\n            type: payload.mimeType ?? \"text/plain;charset=utf-8\",\n          });\n    const url = URL.createObjectURL(blob);\n    try {\n      clickDownload(url, action.fileName);\n    } finally {\n      URL.revokeObjectURL(url);\n    }\n  } catch (error) {\n    throw new ViewerDownloadError({\n      actionId: action.id,\n      kind: \"unsupported\",\n      message: \"Could not start this download.\",\n      cause: error,\n    });\n  }\n}\n\nexport type ViewerDownloadErrorHandler = (\n  error: ViewerDownloadError,\n  action: ViewerDownloadAction,\n) => void;\n\nexport interface ViewerDownloadTrigger {\n  pendingActionId: string | null;\n  triggerDownload: (action: ViewerDownloadAction) => void;\n}\n\nexport interface ViewerDownloadTriggerOptions {\n  /** Reports non-aborted action failures; visible failure UI belongs to consumers. */\n  onError?: ViewerDownloadErrorHandler;\n  resetKey?: unknown;\n}\n\nexport interface ViewerDownloadControlProps {\n  actions: Array<ViewerDownloadAction | null | undefined>;\n  variant?: React.ComponentProps<typeof Button>[\"variant\"];\n  size?: React.ComponentProps<typeof Button>[\"size\"];\n  className?: string;\n  showLabel?: boolean;\n  /** Reports non-aborted action failures; visible failure UI belongs to consumers. */\n  onError?: ViewerDownloadErrorHandler;\n}\n\nexport interface ViewerDownloadButtonProps\n  extends Omit<ViewerDownloadControlProps, \"actions\"> {\n  action: ViewerDownloadAction | null;\n}\n\nexport interface ViewerDownloadMenuProps\n  extends Omit<ViewerDownloadControlProps, \"actions\"> {\n  actions: ViewerDownloadAction[];\n}\n\nexport function useViewerDownloadHref(\n  action: ViewerDownloadAction | null,\n): string | null {\n  const shouldCreateHref = action?.origin !== \"derived\";\n  const payload = shouldCreateHref ? getSynchronousPayload(action) : null;\n\n  return shouldCreateHref && payload?.kind === \"href\" ? payload.href : null;\n}\n\nexport function useViewerDownloadTrigger({\n  onError,\n  resetKey = \"\",\n}: ViewerDownloadTriggerOptions = {}): ViewerDownloadTrigger {\n  const [pendingActionId, setPendingActionId] = React.useState<string | null>(\n    null,\n  );\n  const abortControllerRef = React.useRef<AbortController | null>(null);\n\n  useKeyedMountEffect(joinEffectKey([resetKey]), () => {\n    return () => {\n      abortControllerRef.current?.abort();\n      abortControllerRef.current = null;\n    };\n  });\n\n  const triggerDownload = React.useCallback(\n    (action: ViewerDownloadAction) => {\n      abortControllerRef.current?.abort();\n      const abortController = new AbortController();\n      abortControllerRef.current = abortController;\n      setPendingActionId(action.id);\n      void triggerViewerDownload(action, { signal: abortController.signal })\n        .catch((error) => {\n          reportDownloadError(error, action, onError);\n        })\n        .finally(() => {\n          if (abortControllerRef.current === abortController) {\n            abortControllerRef.current = null;\n            setPendingActionId(null);\n          }\n        });\n    },\n    [onError],\n  );\n\n  return { pendingActionId, triggerDownload };\n}\n\nexport function ViewerDownloadControl({\n  actions,\n  variant = \"ghost\",\n  size = \"iconSm\",\n  className = \"size-7\",\n  showLabel = false,\n  onError,\n}: ViewerDownloadControlProps) {\n  const enabledActions = actions.filter(\n    (action): action is ViewerDownloadAction => Boolean(action),\n  );\n\n  if (enabledActions.length <= 1) {\n    return (\n      <ViewerDownloadButton\n        action={enabledActions[0] ?? null}\n        variant={variant}\n        size={size}\n        className={className}\n        showLabel={showLabel}\n        onError={onError}\n      />\n    );\n  }\n\n  return (\n    <ViewerDownloadMenu\n      actions={enabledActions}\n      variant={variant}\n      size={size}\n      className={className}\n      showLabel={showLabel}\n      onError={onError}\n    />\n  );\n}\n\nexport function ViewerDownloadButton({\n  action,\n  variant = \"ghost\",\n  size = \"iconSm\",\n  className = \"size-7\",\n  showLabel = false,\n  onError,\n}: ViewerDownloadButtonProps) {\n  const href = useViewerDownloadHref(action);\n  const label = action?.label ?? \"Download\";\n  const disabled = !action || action.isDisabled;\n  const hasDownloadHref = Boolean(href);\n  const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({\n    onError,\n    resetKey: action,\n  });\n  const isPending = Boolean(action && pendingActionId === action.id);\n\n  const handleClick = React.useCallback(() => {\n    if (!action || hasDownloadHref) return;\n    triggerDownload(action);\n  }, [action, hasDownloadHref, triggerDownload]);\n\n  if (href) {\n    return (\n      <a\n        href={href}\n        download={action?.fileName}\n        className={cn(buttonVariants({ variant, size }), className)}\n        aria-label={label}\n        title={label}\n        data-slot=\"button\"\n      >\n        <Download className={showLabel ? \"mr-1.5 size-4\" : undefined} />\n        {showLabel ? label : null}\n      </a>\n    );\n  }\n\n  return (\n    <Button\n      variant={variant}\n      size={size}\n      className={className}\n      aria-label={label}\n      title={label}\n      disabled={disabled || isPending}\n      onClick={handleClick}\n    >\n      {isPending ? (\n        <Spinner className=\"size-4 animate-spin\" />\n      ) : (\n        <Download className={showLabel ? \"mr-1.5 size-4\" : undefined} />\n      )}\n      {showLabel ? label : null}\n    </Button>\n  );\n}\n\nexport function ViewerDownloadMenu({\n  actions,\n  variant = \"ghost\",\n  size = \"iconSm\",\n  className = \"size-7\",\n  showLabel = false,\n  onError,\n}: ViewerDownloadMenuProps) {\n  const actionSetKey = actions.map((action) => action.id).join(\"\\u0000\");\n  const label = \"Download\";\n  const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({\n    onError,\n    resetKey: actionSetKey,\n  });\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant={variant}\n          size={size}\n          className={className}\n          aria-label={label}\n          title={label}\n          disabled={pendingActionId != null}\n        >\n          {pendingActionId != null ? (\n            <Spinner className=\"size-4 animate-spin\" />\n          ) : (\n            <Download className={showLabel ? \"mr-1.5 size-4\" : undefined} />\n          )}\n          {showLabel ? label : null}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        {actions.map((action) => (\n          <DropdownMenuItem\n            key={action.id}\n            disabled={action.isDisabled || pendingActionId != null}\n            onClick={() => triggerDownload(action)}\n          >\n            <Download />\n            <span>{action.label}</span>\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction clickDownload(href: string, fileName: string) {\n  const anchor = document.createElement(\"a\");\n  anchor.href = href;\n  anchor.download = fileName;\n  anchor.rel = \"noreferrer\";\n  document.body.appendChild(anchor);\n  anchor.click();\n  anchor.remove();\n}\n\nfunction getSynchronousPayload(\n  action: ViewerDownloadAction | null,\n): ViewerDownloadPayload | null {\n  if (!action || action.isDisabled) return null;\n  const payload = action.getPayload();\n  return payload instanceof Promise ? null : payload;\n}\n\nfunction reportDownloadError(\n  error: unknown,\n  action: ViewerDownloadAction,\n  onError: ViewerDownloadErrorHandler | undefined,\n) {\n  if (!(error instanceof ViewerDownloadError)) return;\n  if (error.kind === \"aborted\") return;\n  onError?.(error, action);\n}\n\nfunction isAbortError(error: unknown) {\n  return error instanceof DOMException && error.name === \"AbortError\";\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-download.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-core.ts",
      "content": "export interface PptxSize {\n  width: number;\n  height: number;\n}\n\nexport interface PptxSlideOverlayProps {\n  /** 1-based slide index. */\n  slideNumber: number;\n  /** Rendered slide size in CSS pixels, after scale and rotation. */\n  width: number;\n  height: number;\n  scale: number;\n  rotation: number;\n}\n\nexport interface PptxSlideRenderTiming {\n  slideNumber: number;\n  durationMs: number;\n  renderScale: number;\n  pixelRatio: number;\n  cached: boolean;\n  status: \"rendered\" | \"cancelled\" | \"failed\";\n}\n\nexport interface PptxSourceLoadTiming {\n  byteLength: number;\n  slideCount: number;\n  totalMs: number;\n  readBytesMs: number;\n  importPptxMs: number;\n  readSlideSizeMs: number;\n  loadFileMs: number;\n  inspectMs: number;\n}\n\nexport interface PptxResetInput {\n  resourceKey: string;\n  scale?: number;\n  defaultScale?: number;\n  eager?: boolean;\n}\n\nexport interface PptxBitmapCacheInput {\n  slideIndex: number;\n  renderScale: number;\n}\n\nexport interface PptxSlideRenderPriority {\n  isCurrentSlide: boolean;\n  isInViewport: boolean;\n  isScrollLead: boolean;\n  distanceFromReadingMarker: number;\n}\n\n// 16:9 — the modern PowerPoint/Slides default. Used as the pre-parse skeleton\n// aspect and as a last-resort fallback when a loaded deck can't report its size.\nexport const DEFAULT_PPTX_SLIDE_SIZE = {\n  width: 960,\n  height: 540,\n} satisfies PptxSize;\n\nexport function getPptxFitScale(\n  containerWidth: number | null,\n  baseWidth: number,\n) {\n  if (!containerWidth || !Number.isFinite(containerWidth) || baseWidth <= 0) {\n    return 1;\n  }\n  return clamp((containerWidth - 32) / baseWidth, 0.1, 5);\n}\n\nexport function getPptxResetKey({\n  resourceKey,\n  scale,\n  defaultScale,\n  eager,\n}: PptxResetInput) {\n  return [\n    resourceKey,\n    getResetScaleKey({ scale, defaultScale }),\n    eager ? \"eager\" : \"settled\",\n  ].join(\"\\u0000\");\n}\n\nfunction getResetScaleKey({\n  scale,\n  defaultScale,\n}: Pick<PptxResetInput, \"scale\" | \"defaultScale\">) {\n  if (scale !== undefined) return normalizePptxScale(scale);\n  if (defaultScale !== undefined) return normalizePptxScale(defaultScale);\n  return \"fit\";\n}\n\nexport function getPptxBitmapCacheKey({\n  slideIndex,\n  renderScale,\n}: PptxBitmapCacheInput) {\n  return `${slideIndex}@${Math.round(renderScale * 1000)}`;\n}\n\nexport function getPptxRenderPixelRatio(rawPixelRatio: number) {\n  if (!Number.isFinite(rawPixelRatio) || rawPixelRatio <= 0) return 1;\n  return Math.min(rawPixelRatio, 2);\n}\n\nexport function getScaledSlideSize(\n  baseSize: PptxSize,\n  zoomScale: number,\n): PptxSize {\n  return {\n    width: baseSize.width * zoomScale,\n    height: baseSize.height * zoomScale,\n  };\n}\n\nexport function getVisibleSlideSize(\n  slideSize: PptxSize,\n  rotation: number,\n): PptxSize {\n  return getRotatedSize(slideSize, rotation);\n}\n\nexport function getRotatedSize(size: PptxSize, rotation: number): PptxSize {\n  const normalized = ((rotation % 360) + 360) % 360;\n  if (normalized === 90 || normalized === 270) {\n    return { width: size.height, height: size.width };\n  }\n  return size;\n}\n\nexport function clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nexport function normalizePptxScale(scale: number) {\n  return clamp(Number.isFinite(scale) ? scale : 1, 0.1, 5);\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-core.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-cache.ts",
      "content": "import { lruGet } from \"./viewer-lru-cache\";\n\nexport interface Disposable {\n  dispose(): void;\n  /**\n   * Optional eviction guard. When it returns false the entry is \"pinned\" and the\n   * LRU will skip it when choosing a victim (evicting the next-oldest evictable\n   * entry instead, or temporarily exceeding the limit if every overflow entry is\n   * pinned). Undefined is treated as always-evictable.\n   */\n  isEvictable?(): boolean;\n}\n\nexport interface DisposableLruBudget<V> {\n  maxCost: number;\n  getCost(value: V): number;\n}\n\nexport class DisposableLruCache<K, V extends Disposable> {\n  private values = new Map<K, V>();\n\n  constructor(\n    private readonly limit: number,\n    private readonly budget?: DisposableLruBudget<V>,\n  ) {}\n\n  get size() {\n    return this.values.size;\n  }\n\n  get(key: K): V | undefined {\n    return lruGet(this.values, key);\n  }\n\n  snapshotValues(): V[] {\n    return [...this.values.values()];\n  }\n\n  set(key: K, value: V) {\n    const existing = this.values.get(key);\n    if (existing) existing.dispose();\n    this.values.delete(key);\n    this.values.set(key, value);\n    this.evictExcess();\n  }\n\n  private evictExcess() {\n    while (this.values.size > this.limit || this.exceedsBudget()) {\n      const victim = this.oldestEvictableKey();\n      // Every overflow entry is pinned (e.g. still loading); keep them all and\n      // let the cache shrink back once they become evictable.\n      if (victim === undefined) break;\n      const dropped = this.values.get(victim);\n      this.values.delete(victim);\n      dropped?.dispose();\n    }\n  }\n\n  private exceedsBudget() {\n    return !!this.budget && this.totalCost() > this.budget.maxCost;\n  }\n\n  private totalCost() {\n    if (!this.budget) return 0;\n    let total = 0;\n    for (const value of this.values.values()) {\n      total += Math.max(0, this.budget.getCost(value));\n    }\n    return total;\n  }\n\n  private oldestEvictableKey(): K | undefined {\n    // Map iterates in LRU order (oldest first), so the first evictable entry is\n    // the least-recently-used one that is safe to drop.\n    for (const [key, value] of this.values) {\n      if (value.isEvictable === undefined || value.isEvictable()) return key;\n    }\n    return undefined;\n  }\n\n  delete(key: K) {\n    const value = this.values.get(key);\n    if (!value) return;\n    this.values.delete(key);\n    value.dispose();\n  }\n\n  clear() {\n    for (const value of this.values.values()) value.dispose();\n    this.values.clear();\n  }\n}\n\nexport class PptxBitmapEntry implements Disposable {\n  constructor(readonly bitmap: ImageBitmap) {}\n\n  get pixelCount() {\n    return Math.max(0, this.bitmap.width * this.bitmap.height);\n  }\n\n  dispose() {\n    this.bitmap.close();\n  }\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-cache.ts"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-lru-cache.ts",
      "content": "export const VIEWER_LRU_CACHE_MAX = 12;\n\nexport function lruGet<K, V>(map: Map<K, V>, key: K): V | undefined {\n  const value = map.get(key);\n  if (value !== undefined) {\n    map.delete(key);\n    map.set(key, value);\n  }\n  return value;\n}\n\nexport function lruSet<K, V>(\n  map: Map<K, V>,\n  key: K,\n  value: V,\n  onEvict?: (key: K, value: V) => void,\n  max = VIEWER_LRU_CACHE_MAX,\n) {\n  map.delete(key);\n  map.set(key, value);\n  while (map.size > max) {\n    const oldest = map.keys().next().value as K | undefined;\n    if (oldest === undefined) break;\n    const dropped = map.get(oldest);\n    map.delete(oldest);\n    if (dropped !== undefined) onEvict?.(oldest, dropped);\n  }\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-lru-cache.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-hooks.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport {\n  type ViewerContentBytes,\n  type ViewerContentIdentity,\n} from \"@/lib/viewer-resource\";\n\nimport { type PptxSourceLoadTiming } from \"./pptx-viewer-core\";\nimport {\n  getPptxSource,\n  subscribePptxSourceLoadTiming,\n  type PptxSource,\n} from \"./pptx-viewer-source\";\n\nconst pptxHookSourceKeys = new WeakMap<PptxSource, string>();\nlet nextPptxHookSourceKey = 1;\n\n/** Retains the cached source for the mounted lifetime of the viewer. */\nexport function useRetainedPptxSource(\n  content: ViewerContentBytes & ViewerContentIdentity,\n  onLoadTiming?: (timing: PptxSourceLoadTiming) => void,\n): PptxSource {\n  const sourcePromise = React.useMemo(() => getPptxSource(content), [content]);\n  const source = React.use(sourcePromise);\n  const onLoadTimingRef = React.useRef(onLoadTiming);\n  onLoadTimingRef.current = onLoadTiming;\n\n  useKeyedMountEffect(getPptxHookSourceKey(source), () => source.retain());\n  useKeyedMountEffect(\n    onLoadTiming ? `timing:${content.sourceKind}:${content.key}` : null,\n    () =>\n      subscribePptxSourceLoadTiming(content, (timing) => {\n        onLoadTimingRef.current?.(timing);\n      }),\n  );\n  return source;\n}\n\nfunction getPptxHookSourceKey(source: PptxSource) {\n  const existingKey = pptxHookSourceKeys.get(source);\n  if (existingKey) return existingKey;\n  const key = String(nextPptxHookSourceKey);\n  nextPptxHookSourceKey += 1;\n  pptxHookSourceKeys.set(source, key);\n  return key;\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-hooks.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-renderer.ts",
      "content": "import type * as PptxNS from \"pptxviewjs\";\n\nimport {\n  isViewerFormatError,\n  ViewerFormatError,\n  type ViewerFormatErrorKind,\n  type ViewerFormatErrorMapperOptions,\n} from \"@/lib/viewer-errors\";\nimport { type ViewerContentBytes } from \"@/lib/viewer-resource\";\n\nimport {\n  DEFAULT_PPTX_SLIDE_SIZE,\n  type PptxSize,\n  type PptxSourceLoadTiming,\n} from \"./pptx-viewer-core\";\n\ntype PptxModule = typeof PptxNS;\n\nexport type PptxRendererErrorKind = ViewerFormatErrorKind;\n\nexport class PptxRendererError extends ViewerFormatError {\n  override readonly kind: PptxRendererErrorKind;\n\n  constructor(kind: PptxRendererErrorKind, message: string, cause?: unknown) {\n    super({ format: \"pptx\", kind, message, cause });\n    this.name = \"PptxRendererError\";\n    this.kind = kind;\n  }\n}\n\nexport interface PptxRenderInput {\n  slideIndex: number;\n  canvas: HTMLCanvasElement;\n  renderScale: number;\n}\n\nexport interface PptxRenderer {\n  slideCount: number;\n  baseSize: PptxSize;\n  renderSlide(input: PptxRenderInput): Promise<void>;\n  dispose(): void;\n}\n\nlet pptxModulePromise: Promise<PptxModule> | null = null;\n\nfunction loadPptx(): Promise<PptxModule> {\n  if (!pptxModulePromise) pptxModulePromise = import(\"pptxviewjs\");\n  return pptxModulePromise;\n}\n\nexport async function createPptxRenderer(\n  content: ViewerContentBytes,\n  onLoadTiming?: (timing: PptxSourceLoadTiming) => void,\n): Promise<PptxRenderer> {\n  const totalStartedAt = now();\n  const readBytesStartedAt = now();\n  const buffer = await readPptxBytes(content);\n  const readBytesMs = now() - readBytesStartedAt;\n\n  const importPptxStartedAt = now();\n  const pptxPromise = loadPptx().then((pptx) => ({\n    pptx,\n    durationMs: now() - importPptxStartedAt,\n  }));\n  const pptx = await pptxPromise;\n  const { PPTXViewer } = pptx.pptx;\n  const offscreen = document.createElement(\"canvas\");\n  const viewer = new PPTXViewer({\n    canvas: offscreen,\n    slideSizeMode: \"actual\",\n    autoExposeGlobals: false,\n    autoChartRerenderDelayMs: 0,\n    enableThumbnails: false,\n  });\n  const loadedViewer = viewer as unknown as LoadedPptxViewer;\n\n  const loadFileStartedAt = now();\n  try {\n    await viewer.loadFile(buffer);\n  } catch (error) {\n    viewer.destroy?.();\n    throw toPptxFormatError(error, {\n      kind: \"load_failed\",\n      message: \"Failed to parse presentation.\",\n    });\n  }\n  const loadFileMs = now() - loadFileStartedAt;\n\n  const readSlideSizeStartedAt = now();\n  const baseSize = readLoadedSlideSize(loadedViewer);\n  const readSlideSizeMs = now() - readSlideSizeStartedAt;\n\n  let slideCount: number;\n  const inspectStartedAt = now();\n  try {\n    slideCount = viewer.getSlideCount();\n  } catch (error) {\n    viewer.destroy?.();\n    throw toPptxFormatError(error, {\n      kind: \"load_failed\",\n      message: \"Failed to inspect presentation slides.\",\n    });\n  }\n  const inspectMs = now() - inspectStartedAt;\n  if (!Number.isInteger(slideCount) || slideCount <= 0) {\n    viewer.destroy?.();\n    throw new PptxRendererError(\n      \"load_failed\",\n      \"Presentation does not contain any slides.\",\n    );\n  }\n\n  onLoadTiming?.({\n    byteLength: buffer.byteLength,\n    importPptxMs: pptx.durationMs,\n    inspectMs,\n    loadFileMs,\n    readBytesMs,\n    readSlideSizeMs,\n    slideCount,\n    totalMs: now() - totalStartedAt,\n  });\n\n  let disposed = false;\n\n  return {\n    slideCount,\n    baseSize,\n    async renderSlide({ slideIndex, canvas, renderScale }) {\n      if (disposed) {\n        throw new PptxRendererError(\n          \"disposed\",\n          \"Presentation renderer was disposed.\",\n        );\n      }\n      if (!isValidSlideIndex(slideIndex, slideCount)) {\n        throw new PptxRendererError(\n          \"index_out_of_range\",\n          `Slide ${slideIndex + 1} is outside the presentation.`,\n        );\n      }\n      if (!isValidRenderScale(renderScale)) {\n        throw new PptxRendererError(\n          \"bounds\",\n          \"Render scale must be a positive finite number.\",\n        );\n      }\n      try {\n        setPptxRenderPixelRatio(loadedViewer, canvas, baseSize, renderScale);\n        await viewer.renderSlide(slideIndex, canvas, {\n          scale: renderScale,\n          quality: \"high\",\n        });\n      } catch (error) {\n        throw toPptxFormatError(error, {\n          kind: \"render_failed\",\n          message: `Failed to render slide ${slideIndex + 1}.`,\n        });\n      }\n    },\n    dispose() {\n      if (disposed) return;\n      disposed = true;\n      viewer.destroy?.();\n    },\n  };\n}\n\nfunction toPptxFormatError(\n  error: unknown,\n  options: ViewerFormatErrorMapperOptions,\n): PptxRendererError {\n  if (error instanceof PptxRendererError) return error;\n  if (isViewerFormatError(error)) {\n    return new PptxRendererError(error.kind, error.message, error.cause);\n  }\n  return new PptxRendererError(options.kind, options.message, error);\n}\n\nfunction readPptxBytes(content: ViewerContentBytes): Promise<ArrayBuffer> {\n  return content.readBytes();\n}\n\ntype LoadedPptxViewer = {\n  getSlideDimensions?: () => unknown;\n  processor?: unknown;\n  presentation?: unknown;\n};\n\ntype LoadedPptxProcessor = {\n  getSlideDimensions?: () => unknown;\n  processor?: unknown;\n  presentation?: unknown;\n  renderContext?: {\n    pixelRatio?: number;\n    dpi?: number;\n  };\n  setPixelRatio?: (ratio: number) => void;\n};\n\nfunction readLoadedSlideSize(viewer: LoadedPptxViewer): PptxSize {\n  return (\n    parseLoadedSlideSize(readLoadedSlideDimensions(viewer)) ??\n    DEFAULT_PPTX_SLIDE_SIZE\n  );\n}\n\nfunction readLoadedSlideDimensions(viewer: LoadedPptxViewer): unknown {\n  try {\n    const dimensions = viewer.getSlideDimensions?.();\n    if (dimensions) return dimensions;\n  } catch {\n    /* fall through to exposed presentation objects */\n  }\n\n  const processor = viewer.processor as LoadedPptxProcessor | undefined;\n  try {\n    const dimensions = processor?.getSlideDimensions?.();\n    if (dimensions) return dimensions;\n  } catch {\n    /* fall through to presentation fields */\n  }\n\n  return (\n    readPresentationSlideSize(viewer.presentation) ??\n    readPresentationSlideSize(processor?.presentation) ??\n    readPresentationSlideSize(\n      (processor?.processor as LoadedPptxProcessor | undefined)?.presentation,\n    )\n  );\n}\n\nfunction readPresentationSlideSize(presentation: unknown): unknown {\n  if (!presentation || typeof presentation !== \"object\") return null;\n  const slideSize = (presentation as { slideSize?: unknown }).slideSize;\n  return slideSize ?? null;\n}\n\nfunction parseLoadedSlideSize(value: unknown): PptxSize | null {\n  if (!value || typeof value !== \"object\") return null;\n  const { cx, cy } = value as { cx?: unknown; cy?: unknown };\n  const widthEmu = Number(cx);\n  const heightEmu = Number(cy);\n  if (\n    !Number.isFinite(widthEmu) ||\n    !Number.isFinite(heightEmu) ||\n    widthEmu <= 0 ||\n    heightEmu <= 0\n  ) {\n    return null;\n  }\n\n  const width = Math.round(widthEmu / 9525);\n  const height = Math.round(heightEmu / 9525);\n  if (width <= 0 || height <= 0) return null;\n  return { width, height };\n}\n\nfunction setPptxRenderPixelRatio(\n  viewer: LoadedPptxViewer,\n  canvas: HTMLCanvasElement,\n  baseSize: PptxSize,\n  renderScale: number,\n) {\n  const logicalWidth = Number.parseFloat(canvas.style.width);\n  const zoomScale =\n    Number.isFinite(logicalWidth) && logicalWidth > 0 && baseSize.width > 0\n      ? logicalWidth / baseSize.width\n      : 1;\n  const pixelRatio = renderScale / zoomScale;\n  if (!Number.isFinite(pixelRatio) || pixelRatio <= 0) return;\n\n  const processor = viewer.processor as LoadedPptxProcessor | undefined;\n  const innerProcessor = processor?.processor as\n    | LoadedPptxProcessor\n    | undefined;\n  setProcessorPixelRatio(processor, pixelRatio);\n  setProcessorPixelRatio(innerProcessor, pixelRatio);\n}\n\nfunction setProcessorPixelRatio(\n  processor: LoadedPptxProcessor | undefined,\n  pixelRatio: number,\n) {\n  if (!processor) return;\n  if (typeof processor.setPixelRatio === \"function\") {\n    try {\n      processor.setPixelRatio(pixelRatio);\n      return;\n    } catch {\n      /* fall through to direct renderContext patching */\n    }\n  }\n  if (!processor.renderContext) return;\n  processor.renderContext.pixelRatio = pixelRatio;\n  processor.renderContext.dpi = pixelRatio * 96;\n}\n\nfunction isValidSlideIndex(slideIndex: number, slideCount: number) {\n  return (\n    Number.isInteger(slideIndex) && slideIndex >= 0 && slideIndex < slideCount\n  );\n}\n\nfunction isValidRenderScale(renderScale: number) {\n  return Number.isFinite(renderScale) && renderScale > 0;\n}\n\nfunction now() {\n  return typeof performance === \"undefined\" ? Date.now() : performance.now();\n}\n\nexport function resetPptxRendererModules() {\n  pptxModulePromise = null;\n}\n\nexport function preloadPptxRenderer() {\n  void loadPptx();\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-renderer.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-scroll.ts",
      "content": "export const PPTX_SCROLL_IDLE_MS = 120;\n\nexport interface PptxScrollActivity {\n  handleScroll(): void;\n  isScrolling(): boolean;\n  onIdle(callback: () => void): () => void;\n}\n\nexport function createPptxScrollActivity(\n  idleMs = PPTX_SCROLL_IDLE_MS,\n): PptxScrollActivity {\n  let isScrolling = false;\n  let timer = 0;\n  const waiters = new Set<() => void>();\n\n  return {\n    handleScroll() {\n      isScrolling = true;\n      clearTimeout(timer);\n      timer = window.setTimeout(() => {\n        isScrolling = false;\n        const pending = [...waiters];\n        waiters.clear();\n        for (const callback of pending) callback();\n      }, idleMs);\n    },\n    isScrolling: () => isScrolling,\n    onIdle(callback: () => void) {\n      waiters.add(callback);\n      return () => waiters.delete(callback);\n    },\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-scroll.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-viewport.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport function usePptxViewportWidth({\n  enabled = true,\n}: {\n  enabled?: boolean;\n} = {}) {\n  const [containerElement, setContainerElement] =\n    React.useState<HTMLDivElement | null>(null);\n  const [viewportWidth, setViewportWidth] = React.useState<number | null>(null);\n\n  useKeyedLayoutEffect(enabled ? null : \"reset\", () => {\n    setViewportWidth((current) => (current == null ? current : null));\n  });\n\n  useKeyedLayoutEffect(\n    enabled && containerElement ? joinEffectKey([containerElement]) : null,\n    () => {\n      if (!containerElement) return;\n\n      let frame = 0;\n      let latest = resolvePptxMeasuredWidth(containerElement.clientWidth);\n      setViewportWidth(latest);\n      if (typeof ResizeObserver === \"undefined\") return;\n\n      let observer: ResizeObserver | null = null;\n      try {\n        observer = new ResizeObserver((entries) => {\n          for (const entry of entries) {\n            latest = resolvePptxMeasuredWidth(\n              (entry.target as HTMLElement).clientWidth,\n            );\n          }\n          if (frame) return;\n          frame = -1;\n          const requestedFrame = requestAnimationFrame(() => {\n            frame = 0;\n            setViewportWidth((current) =>\n              current === latest ? current : latest,\n            );\n          });\n          if (frame === -1) frame = requestedFrame;\n        });\n        observer.observe(containerElement);\n      } catch {\n        if (frame > 0) cancelAnimationFrame(frame);\n        observer?.disconnect();\n        /* Keep the initial measurement when ResizeObserver is unavailable at runtime. */\n        return;\n      }\n\n      return () => {\n        if (frame > 0) cancelAnimationFrame(frame);\n        observer?.disconnect();\n      };\n    },\n  );\n\n  return { containerRef: setContainerElement, viewportWidth };\n}\n\nfunction resolvePptxMeasuredWidth(value: number) {\n  return Number.isFinite(value) && value > 0 ? value : null;\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-viewport.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-visible-slide.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\n\nimport {\n  clamp,\n  getScaledSlideSize,\n  getVisibleSlideSize,\n  type PptxSize,\n} from \"./pptx-viewer-core\";\nimport {\n  PPTX_ZOOM_INTENT_MAX_AGE_MS,\n  type PptxZoomTransaction,\n} from \"./pptx-viewer-zoom-motion\";\nimport type { ViewerDocumentZoomMotionController } from \"./viewer-types\";\n\nexport const PPTX_READING_MARKER_RATIO = 0.2;\nexport const PPTX_RENDER_FIT_PERFECTLY_OVERSCAN_PX = 32;\nexport const PPTX_RENDER_WINDOW_OVERSCAN_PX = 1000;\nexport const PPTX_SCROLL_REBASE_CONTAINER_PX = 12_000_000;\nexport const PPTX_SCROLL_REBASE_TRIGGER_PX = 1_000_000;\nexport const PPTX_SCROLL_REBASE_TARGET_PX = 2_000_000;\nexport const PPTX_SCROLL_REBASE_TARGET_BOTTOM_PX =\n  PPTX_SCROLL_REBASE_CONTAINER_PX - PPTX_SCROLL_REBASE_TARGET_PX;\nexport const PPTX_SCROLL_REBASE_THRESHOLD_PX =\n  PPTX_SCROLL_REBASE_CONTAINER_PX - PPTX_SCROLL_REBASE_TRIGGER_PX;\nexport const PPTX_SCROLL_POSITION_EPSILON = 1;\n\nexport interface PptxSlideLayout {\n  slideCount: number;\n  slideTopPadding: number;\n  slideGap: number;\n  slideHeight: number;\n  slideWidth: number;\n  slideStride: number;\n  totalHeight: number;\n}\n\nexport interface PptxVirtualSlide {\n  height: number;\n  index: number;\n  key: string;\n  slideNumber: number;\n  top: number;\n  width: number;\n}\n\nexport interface PptxRenderedSlideWindow {\n  afterHeight: number;\n  beforeHeight: number;\n  height: number;\n  slides: Array<PptxVirtualSlide & { windowTop: number }>;\n  stickyBottomInset: number;\n  stickyTopInset: number;\n}\n\nexport interface PptxRenderPixelWindow {\n  bottom: number;\n  top: number;\n}\n\nexport interface PptxScrollMetrics {\n  physicalScrollHeight: number;\n  physicalScrollTop: number;\n  scrollPageOffset: number;\n  scrollTop: number;\n  viewportHeight: number;\n}\n\nexport interface PptxScrollRebasePosition {\n  physicalScrollTop: number;\n  scrollPageOffset: number;\n}\n\nexport interface PptxSlideLayoutInput {\n  baseSize: PptxSize;\n  zoomScale: number;\n  rotation: number;\n  slideCount: number;\n  slideGap: number;\n  slidePadding: number;\n}\n\nexport interface PptxVisibleSlideInput {\n  layout: PptxSlideLayout;\n  onVisibleSlideChange?: (slide: number) => void;\n  onScrollProgressChange?: (progress: number) => void;\n  zoomMotion?: ViewerDocumentZoomMotionController<PptxZoomTransaction>;\n}\n\ntype PptxReadingAnchor =\n  | {\n      kind: \"top\";\n    }\n  | {\n      kind: \"slide\";\n      slideNumber: number;\n      yPercent: number;\n    };\n\nexport function createPptxSlideLayout({\n  baseSize,\n  zoomScale,\n  rotation,\n  slideCount,\n  slideGap,\n  slidePadding,\n}: PptxSlideLayoutInput): PptxSlideLayout {\n  const slideSize = getScaledSlideSize(baseSize, zoomScale);\n  const visibleSize = getVisibleSlideSize(slideSize, rotation);\n  const normalizedSlideCount = Number.isFinite(slideCount)\n    ? Math.max(0, Math.floor(slideCount))\n    : 0;\n  // The gap scales with the slide, while the outer viewer inset stays fixed so\n  // the first loaded slide occupies the exact p-4 skeleton frame. NOT rounded:\n  // slide dimensions\n  // (getScaledSlideSize) are fractional, so leaving the gap fractional too\n  // keeps gap / slide height exactly constant and the layout perfectly linear.\n  // (PDF rounds its gap only because its page dimensions are already\n  // integer-rounded; rounding here would gain no integer offsets, just error.)\n  const safeZoomScale =\n    Number.isFinite(zoomScale) && zoomScale > 0 ? zoomScale : 1;\n  const normalizedSlideGap =\n    Number.isFinite(slideGap) && slideGap > 0 ? slideGap * safeZoomScale : 0;\n  const normalizedSlidePadding =\n    Number.isFinite(slidePadding) && slidePadding > 0 ? slidePadding : 0;\n  const gapCount = Math.max(0, normalizedSlideCount - 1);\n\n  return {\n    slideCount: normalizedSlideCount,\n    slideTopPadding: normalizedSlidePadding,\n    slideGap: normalizedSlideGap,\n    slideHeight: visibleSize.height,\n    slideWidth: visibleSize.width,\n    slideStride: visibleSize.height + normalizedSlideGap,\n    totalHeight:\n      normalizedSlidePadding * 2 +\n      visibleSize.height * normalizedSlideCount +\n      normalizedSlideGap * gapCount,\n  };\n}\n\nexport function getPptxSlideAtScrollMarker(\n  layout: PptxSlideLayout,\n  markerScrollTop: number,\n) {\n  if (layout.slideCount <= 1 || layout.slideStride <= 0) return 1;\n\n  const slideIndex = Math.floor(\n    (markerScrollTop - layout.slideTopPadding) / layout.slideStride,\n  );\n  return clamp(slideIndex + 1, 1, layout.slideCount);\n}\n\nexport function getPptxSlideTop(layout: PptxSlideLayout, slideIndex: number) {\n  return layout.slideTopPadding + slideIndex * layout.slideStride;\n}\n\nexport function getPptxVirtualSlides({\n  layout,\n  overscanSlides = 2,\n  scrollTop,\n  viewportHeight,\n}: {\n  layout: PptxSlideLayout;\n  overscanSlides?: number;\n  scrollTop: number;\n  viewportHeight: number;\n}): PptxVirtualSlide[] {\n  if (layout.slideCount === 0) return [];\n\n  const safeViewportHeight =\n    Number.isFinite(viewportHeight) && viewportHeight > 0\n      ? viewportHeight\n      : layout.slideHeight;\n  const safeScrollTop =\n    Number.isFinite(scrollTop) && scrollTop > 0 ? scrollTop : 0;\n  const safeOverscanSlides =\n    Number.isFinite(overscanSlides) && overscanSlides > 0\n      ? Math.floor(overscanSlides)\n      : 0;\n  const firstVisibleIndex =\n    layout.slideStride > 0\n      ? Math.floor(\n          (safeScrollTop - layout.slideTopPadding) / layout.slideStride,\n        )\n      : 0;\n  const lastVisibleIndex =\n    layout.slideStride > 0\n      ? Math.floor(\n          (safeScrollTop + safeViewportHeight - layout.slideTopPadding) /\n            layout.slideStride,\n        )\n      : 0;\n  const firstIndex = clamp(\n    firstVisibleIndex - safeOverscanSlides,\n    0,\n    layout.slideCount - 1,\n  );\n  const lastIndex = clamp(\n    lastVisibleIndex + safeOverscanSlides,\n    0,\n    layout.slideCount - 1,\n  );\n\n  return Array.from(\n    { length: lastIndex - firstIndex + 1 },\n    (_, offset): PptxVirtualSlide => {\n      const index = firstIndex + offset;\n      const slideNumber = index + 1;\n      return {\n        height: layout.slideHeight,\n        index,\n        key: String(slideNumber),\n        slideNumber,\n        top: getPptxSlideTop(layout, index),\n        width: layout.slideWidth,\n      };\n    },\n  );\n}\n\nexport function getPptxRenderSlides({\n  fitPerfectly = false,\n  fitPerfectlyOverscanPx = PPTX_RENDER_FIT_PERFECTLY_OVERSCAN_PX,\n  layout,\n  overscanPx = PPTX_RENDER_WINDOW_OVERSCAN_PX,\n  scrollTop,\n  viewportHeight,\n}: {\n  fitPerfectly?: boolean;\n  fitPerfectlyOverscanPx?: number;\n  layout: PptxSlideLayout;\n  overscanPx?: number;\n  scrollTop: number;\n  viewportHeight: number;\n}): PptxVirtualSlide[] {\n  if (layout.slideCount === 0) return [];\n\n  const window = createPptxWindowFromScrollPosition({\n    fitPerfectly,\n    fitPerfectlyOverscanPx,\n    overscanPx,\n    scrollHeight: layout.totalHeight,\n    scrollTop,\n    viewportHeight,\n  });\n\n  return getPptxSlidesInRange({\n    layout,\n    startOffset: window.top,\n    endOffset: window.bottom,\n  });\n}\n\nexport function getPptxRenderedSlideWindow({\n  layout,\n  physicalScrollHeight = layout.totalHeight,\n  scrollPageOffset = 0,\n  slides,\n  viewportHeight,\n}: {\n  layout: PptxSlideLayout;\n  physicalScrollHeight?: number;\n  scrollPageOffset?: number;\n  slides: readonly PptxVirtualSlide[];\n  viewportHeight: number;\n}): PptxRenderedSlideWindow | null {\n  const renderedSlides = [...slides].sort((a, b) => a.index - b.index);\n  if (renderedSlides.length === 0) return null;\n\n  const logicalBeforeHeight = renderedSlides[0]?.top ?? 0;\n  const windowBottom = Math.max(\n    ...renderedSlides.map((slide) => slide.top + slide.height),\n  );\n  const height = Math.max(0, windowBottom - logicalBeforeHeight);\n  const beforeHeight = getPptxPagedLayoutTop({\n    logicalTop: logicalBeforeHeight,\n    scrollPageOffset,\n    totalHeight: layout.totalHeight,\n    viewportHeight,\n  });\n  const safeViewportHeight =\n    Number.isFinite(viewportHeight) && viewportHeight > 0\n      ? viewportHeight\n      : layout.slideHeight > 0\n        ? layout.slideHeight\n        : 0;\n  const stickyCoverInset =\n    safeViewportHeight > 0 ? -Math.max(0, height - safeViewportHeight) : 0;\n  const afterHeight = Math.max(\n    0,\n    safeSize(physicalScrollHeight) - beforeHeight - height,\n  );\n  // The sticky cover clamp only ever needs to slide the window over CONTENT\n  // the viewport can reach. When the window already holds the document's\n  // first/last slide, the spacer beyond it is pure edge padding — relax that\n  // side's inset by the spacer so the settled window keeps its flow position\n  // at the scroll extremes. Without the relaxation the clamp drags the window\n  // over the edge padding at scroll 0/max, and the settled layout stops being\n  // a single linear function of scale (which the reading-fraction rebase\n  // depends on to restore the reading line exactly across a re-fit).\n  const includesFirstSlide = renderedSlides[0].index === 0;\n  const includesLastSlide =\n    renderedSlides[renderedSlides.length - 1].index === layout.slideCount - 1;\n\n  return {\n    afterHeight,\n    beforeHeight,\n    height,\n    slides: renderedSlides.map((slide) => ({\n      ...slide,\n      windowTop: slide.top - logicalBeforeHeight,\n    })),\n    stickyBottomInset:\n      stickyCoverInset - (includesFirstSlide ? beforeHeight : 0),\n    stickyTopInset: stickyCoverInset - (includesLastSlide ? afterHeight : 0),\n  };\n}\n\nexport function createPptxWindowFromScrollPosition({\n  fitPerfectly = false,\n  fitPerfectlyOverscanPx = 0,\n  overscanPx,\n  scrollHeight,\n  scrollTop,\n  viewportHeight,\n}: {\n  fitPerfectly?: boolean;\n  fitPerfectlyOverscanPx?: number;\n  overscanPx: number;\n  scrollHeight: number;\n  scrollTop: number;\n  viewportHeight: number;\n}): PptxRenderPixelWindow {\n  const safeOverscanPx = safePadding(overscanPx);\n  const safeScrollHeight = safeSize(scrollHeight);\n  const safeScrollTop = Math.max(0, finiteNumber(scrollTop));\n  const safeViewportHeight = Math.max(0, finiteNumber(viewportHeight));\n  const windowHeight = safeViewportHeight + safeOverscanPx * 2;\n  const fitPerfectlyOverscan = safePadding(fitPerfectlyOverscanPx);\n  const effectiveHeight = fitPerfectly\n    ? safeViewportHeight + fitPerfectlyOverscan * 2\n    : windowHeight;\n\n  if (windowHeight >= safeScrollHeight || fitPerfectly) {\n    const fitScrollTop = Math.min(safeScrollTop, safeScrollHeight);\n    const top = Math.max(fitScrollTop - fitPerfectlyOverscan, 0);\n    const bottom = Math.min(fitScrollTop + effectiveHeight, safeScrollHeight);\n    return {\n      bottom: Math.ceil(Math.max(bottom, top)),\n      top: Math.floor(Math.max(0, top)),\n    };\n  }\n\n  const scrollCenter = safeScrollTop + 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 > safeScrollHeight) {\n    bottom = safeScrollHeight;\n    top = safeScrollHeight - 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 getPptxPhysicalScrollHeight({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  const safeTotalHeight = safeSize(totalHeight);\n  return shouldRebasePptxScroll({\n    totalHeight: safeTotalHeight,\n    viewportHeight,\n  })\n    ? Math.min(safeTotalHeight, PPTX_SCROLL_REBASE_CONTAINER_PX)\n    : safeTotalHeight;\n}\n\nexport function getPptxLogicalScrollTop({\n  physicalScrollTop,\n  scrollPageOffset,\n  totalHeight,\n  viewportHeight,\n}: {\n  physicalScrollTop: number;\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return clamp(\n    finiteNumber(physicalScrollTop) + safePadding(scrollPageOffset),\n    0,\n    getPptxMaxLogicalScrollTop({ totalHeight, viewportHeight }),\n  );\n}\n\nexport function resolvePptxPhysicalScrollPosition({\n  logicalScrollTop,\n  scrollPageOffset,\n  totalHeight,\n  viewportHeight,\n}: {\n  logicalScrollTop: number;\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportHeight: number;\n}): PptxScrollRebasePosition {\n  const safeLogicalScrollTop = clamp(\n    finiteNumber(logicalScrollTop),\n    0,\n    getPptxMaxLogicalScrollTop({ totalHeight, viewportHeight }),\n  );\n\n  if (!shouldRebasePptxScroll({ totalHeight, viewportHeight })) {\n    return {\n      physicalScrollTop: clamp(\n        safeLogicalScrollTop,\n        0,\n        getPptxMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n      ),\n      scrollPageOffset: 0,\n    };\n  }\n\n  const currentPageOffset = clampPptxScrollPageOffset({\n    scrollPageOffset,\n    totalHeight,\n    viewportHeight,\n  });\n  const physicalScrollTop = safeLogicalScrollTop - currentPageOffset;\n  const maxPhysicalScrollTop = getPptxMaxPhysicalScrollTop({\n    totalHeight,\n    viewportHeight,\n  });\n  const maxPageOffset = getPptxMaxScrollPageOffset({\n    totalHeight,\n    viewportHeight,\n  });\n  const shouldMoveDown =\n    physicalScrollTop > PPTX_SCROLL_REBASE_THRESHOLD_PX &&\n    currentPageOffset < maxPageOffset;\n  const shouldMoveUp =\n    physicalScrollTop < PPTX_SCROLL_REBASE_TRIGGER_PX && currentPageOffset > 0;\n\n  if (\n    physicalScrollTop < 0 ||\n    physicalScrollTop > maxPhysicalScrollTop ||\n    shouldMoveDown ||\n    shouldMoveUp\n  ) {\n    return resolvePptxScrollPageWindow({\n      logicalScrollTop: safeLogicalScrollTop,\n      preferredPhysicalScrollTop: shouldMoveUp\n        ? Math.min(PPTX_SCROLL_REBASE_TARGET_BOTTOM_PX, maxPhysicalScrollTop)\n        : PPTX_SCROLL_REBASE_TARGET_PX,\n      totalHeight,\n      viewportHeight,\n    });\n  }\n\n  return {\n    physicalScrollTop: roundPptxScrollPixel(\n      clamp(physicalScrollTop, 0, maxPhysicalScrollTop),\n    ),\n    scrollPageOffset: currentPageOffset,\n  };\n}\n\nexport function getPptxPagedLayoutTop({\n  logicalTop,\n  scrollPageOffset,\n  totalHeight,\n  viewportHeight,\n}: {\n  logicalTop: number;\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  if (!shouldRebasePptxScroll({ totalHeight, viewportHeight })) {\n    return finiteNumber(logicalTop);\n  }\n  return Math.max(0, finiteNumber(logicalTop) - safePadding(scrollPageOffset));\n}\n\nexport function readPptxScrollMetrics({\n  scrollPageOffset,\n  totalHeight,\n  viewportElement,\n}: {\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportElement: HTMLDivElement | null;\n}): PptxScrollMetrics {\n  const viewportHeight = viewportElement?.clientHeight ?? 0;\n  const physicalScrollTop = viewportElement?.scrollTop ?? 0;\n  const physicalScrollHeight = getPptxPhysicalScrollHeight({\n    totalHeight,\n    viewportHeight,\n  });\n  const isRebased = physicalScrollHeight < totalHeight;\n\n  return {\n    physicalScrollHeight,\n    physicalScrollTop,\n    scrollPageOffset: isRebased ? scrollPageOffset : 0,\n    scrollTop: isRebased\n      ? getPptxLogicalScrollTop({\n          physicalScrollTop,\n          scrollPageOffset,\n          totalHeight,\n          viewportHeight,\n        })\n      : Math.max(0, physicalScrollTop),\n    viewportHeight,\n  };\n}\n\nexport function usePptxVisibleSlide({\n  layout,\n  onVisibleSlideChange,\n  onScrollProgressChange,\n  zoomMotion,\n}: PptxVisibleSlideInput) {\n  const [currentSlide, setCurrentSlide] = React.useState(1);\n  const scrollViewportRef = React.useRef<HTMLDivElement | null>(null);\n  const lastReportedSlide = React.useRef(0);\n  const lastVisibleSlideCallback = React.useRef(onVisibleSlideChange);\n  const committedLayoutRef = React.useRef(layout);\n  const scrollPageOffsetRef = React.useRef(0);\n  const pendingZoomIntentRef = React.useRef<{\n    capturedAt: number;\n    transaction: PptxZoomTransaction;\n  } | null>(null);\n  const activeZoomMotionCancelRef = React.useRef<(() => void) | null>(null);\n  const layoutEffectKey = getPptxSlideLayoutKey(layout);\n\n  // Interrupting a zoom relax snaps to its committed endpoint: the layout and\n  // scroll landed in the zoom's own commit, so clearing the transform is\n  // always safe and never moves the settled geometry.\n  const cancelZoomMotion = React.useCallback(() => {\n    pendingZoomIntentRef.current = null;\n    const cancelActiveZoomMotion = activeZoomMotionCancelRef.current;\n    activeZoomMotionCancelRef.current = null;\n    cancelActiveZoomMotion?.();\n  }, []);\n\n  if (lastVisibleSlideCallback.current !== onVisibleSlideChange) {\n    lastVisibleSlideCallback.current = onVisibleSlideChange;\n    lastReportedSlide.current = 0;\n  }\n\n  const getScrollMetrics = React.useCallback(\n    () =>\n      readPptxScrollMetrics({\n        scrollPageOffset: scrollPageOffsetRef.current,\n        totalHeight: layout.totalHeight,\n        viewportElement: scrollViewportRef.current,\n      }),\n    [layout.totalHeight],\n  );\n\n  // Called in the zoom gesture's own task, against the pre-zoom layout and\n  // painted DOM; the layout commit the gesture causes consumes the intent.\n  const captureZoomIntent = React.useCallback(() => {\n    const viewport = scrollViewportRef.current;\n    if (!viewport || !zoomMotion) {\n      pendingZoomIntentRef.current = null;\n      return;\n    }\n    const metrics = readPptxScrollMetrics({\n      scrollPageOffset: scrollPageOffsetRef.current,\n      totalHeight: layout.totalHeight,\n      viewportElement: viewport,\n    });\n    const transaction = zoomMotion.capture({\n      scrollTop: metrics.scrollTop,\n      viewportElement: viewport,\n    });\n    pendingZoomIntentRef.current =\n      transaction == null\n        ? null\n        : { capturedAt: readPptxZoomNow(), transaction };\n  }, [layout.totalHeight, zoomMotion]);\n\n  const syncPhysicalScrollPosition = React.useCallback(\n    (viewport: HTMLDivElement) => {\n      const metrics = readPptxScrollMetrics({\n        scrollPageOffset: scrollPageOffsetRef.current,\n        totalHeight: layout.totalHeight,\n        viewportElement: viewport,\n      });\n      if (metrics.physicalScrollHeight >= layout.totalHeight) {\n        scrollPageOffsetRef.current = 0;\n        return {\n          ...metrics,\n          scrollPageOffset: 0,\n        };\n      }\n\n      const position = resolvePptxPhysicalScrollPosition({\n        logicalScrollTop: metrics.scrollTop,\n        scrollPageOffset: metrics.scrollPageOffset,\n        totalHeight: layout.totalHeight,\n        viewportHeight: metrics.viewportHeight,\n      });\n      scrollPageOffsetRef.current = position.scrollPageOffset;\n      setViewportPhysicalScrollTop(viewport, position.physicalScrollTop);\n\n      return {\n        ...metrics,\n        physicalScrollTop: position.physicalScrollTop,\n        scrollPageOffset: position.scrollPageOffset,\n      };\n    },\n    [layout.totalHeight],\n  );\n\n  const scrollViewportToLogicalTop = React.useCallback(\n    (viewport: HTMLDivElement, targetTop: number) => {\n      const position = resolvePptxPhysicalScrollPosition({\n        logicalScrollTop: targetTop,\n        scrollPageOffset: scrollPageOffsetRef.current,\n        totalHeight: layout.totalHeight,\n        viewportHeight: viewport.clientHeight,\n      });\n      scrollPageOffsetRef.current = position.scrollPageOffset;\n      setViewportPhysicalScrollTop(viewport, position.physicalScrollTop);\n    },\n    [layout.totalHeight],\n  );\n\n  useKeyedLayoutEffect(layoutEffectKey, () => {\n    const previousLayout = committedLayoutRef.current;\n    committedLayoutRef.current = layout;\n\n    if (arePptxSlideLayoutsEqual(previousLayout, layout)) return;\n\n    const viewport = scrollViewportRef.current;\n    if (!viewport) return;\n\n    // A fresh layout commit owns the visual layer; an in-flight zoom relax\n    // against the previous layout can no longer settle correctly.\n    const pendingZoomIntent = pendingZoomIntentRef.current;\n    pendingZoomIntentRef.current = null;\n    cancelZoomMotion();\n\n    if (\n      pendingZoomIntent &&\n      zoomMotion &&\n      readPptxZoomNow() - pendingZoomIntent.capturedAt <=\n        PPTX_ZOOM_INTENT_MAX_AGE_MS\n    ) {\n      const zoomTarget = zoomMotion.resolveScrollTarget({\n        transaction: pendingZoomIntent.transaction,\n        viewportElement: viewport,\n      });\n      if (zoomTarget) {\n        // Commit-then-relax: land the centered scroll inside this commit\n        // (through the paged-scroll mapping), then relax the painted FLIP\n        // over it. Raw scrollLeft assignment on purpose — the browser clamps\n        // to the live scrollable range, including RTL's negative space.\n        scrollViewportToLogicalTop(viewport, zoomTarget.top);\n        if (zoomTarget.left != null && Number.isFinite(zoomTarget.left)) {\n          viewport.scrollLeft = zoomTarget.left;\n        }\n        activeZoomMotionCancelRef.current = zoomMotion.play({\n          transaction: pendingZoomIntent.transaction,\n          viewportElement: viewport,\n        });\n        return;\n      }\n    }\n\n    const previousLogicalScrollTop = getPptxLogicalScrollTop({\n      physicalScrollTop: viewport.scrollTop,\n      scrollPageOffset: scrollPageOffsetRef.current,\n      totalHeight: previousLayout.totalHeight,\n      viewportHeight: viewport.clientHeight,\n    });\n    const anchor = capturePptxReadingAnchor(\n      previousLayout,\n      viewport,\n      previousLogicalScrollTop,\n    );\n    if (!anchor) return;\n\n    const targetTop = getPptxReadingAnchorScrollTop(layout, viewport, anchor);\n    if (targetTop != null) scrollViewportToLogicalTop(viewport, targetTop);\n  });\n\n  const handleScroll = React.useCallback(() => {\n    const viewport = scrollViewportRef.current;\n    if (!viewport) return;\n\n    const metrics = syncPhysicalScrollPosition(viewport);\n    const isRebased = metrics.physicalScrollHeight < layout.totalHeight;\n    const scrollable = isRebased\n      ? layout.totalHeight - metrics.viewportHeight\n      : viewport.scrollHeight - metrics.viewportHeight;\n    onScrollProgressChange?.(\n      scrollable > 0\n        ? clamp(\n            (isRebased ? metrics.scrollTop : viewport.scrollTop) / scrollable,\n            0,\n            1,\n          )\n        : 0,\n    );\n\n    const markerScrollTop =\n      metrics.scrollTop + metrics.viewportHeight * PPTX_READING_MARKER_RATIO;\n    const visibleSlide = getPptxSlideAtScrollMarker(layout, markerScrollTop);\n\n    if (visibleSlide && visibleSlide !== lastReportedSlide.current) {\n      lastReportedSlide.current = visibleSlide;\n      setCurrentSlide(visibleSlide);\n      onVisibleSlideChange?.(visibleSlide);\n    }\n  }, [\n    layout,\n    onScrollProgressChange,\n    onVisibleSlideChange,\n    syncPhysicalScrollPosition,\n  ]);\n\n  return {\n    captureZoomIntent,\n    currentSlide,\n    getScrollMetrics,\n    handleScroll,\n    scrollViewportRef,\n  };\n}\n\nfunction readPptxZoomNow() {\n  return typeof performance !== \"undefined\" &&\n    typeof performance.now === \"function\"\n    ? performance.now()\n    : Date.now();\n}\n\nfunction getPptxSlideLayoutKey(layout: PptxSlideLayout) {\n  return [\n    layout.slideCount,\n    layout.slideTopPadding,\n    layout.slideGap,\n    layout.slideHeight,\n    layout.slideWidth,\n    layout.slideStride,\n    layout.totalHeight,\n  ].join(\"\\u0000\");\n}\n\nfunction capturePptxReadingAnchor(\n  layout: PptxSlideLayout,\n  viewport: HTMLDivElement,\n  scrollTop: number,\n): PptxReadingAnchor | null {\n  if (layout.slideCount === 0) return null;\n  if (scrollTop <= 0) return { kind: \"top\" };\n\n  const markerScrollTop =\n    scrollTop + viewport.clientHeight * PPTX_READING_MARKER_RATIO;\n  const slideNumber = getPptxSlideAtScrollMarker(layout, markerScrollTop);\n  const slideTop = getPptxSlideTop(layout, slideNumber - 1);\n  if (layout.slideHeight <= 0) return null;\n\n  return {\n    kind: \"slide\",\n    slideNumber,\n    yPercent: clamp((markerScrollTop - slideTop) / layout.slideHeight, 0, 1),\n  };\n}\n\nfunction getPptxReadingAnchorScrollTop(\n  layout: PptxSlideLayout,\n  viewport: HTMLDivElement,\n  anchor: PptxReadingAnchor,\n) {\n  if (anchor.kind === \"top\") {\n    return 0;\n  }\n\n  if (anchor.slideNumber < 1 || anchor.slideNumber > layout.slideCount) {\n    return null;\n  }\n\n  const slideTop = getPptxSlideTop(layout, anchor.slideNumber - 1);\n  const targetTop =\n    slideTop +\n    layout.slideHeight * anchor.yPercent -\n    viewport.clientHeight * PPTX_READING_MARKER_RATIO;\n  const maxScrollTop = Math.max(0, layout.totalHeight - viewport.clientHeight);\n  return clamp(targetTop, 0, maxScrollTop);\n}\n\nfunction arePptxSlideLayoutsEqual(\n  previousLayout: PptxSlideLayout,\n  nextLayout: PptxSlideLayout,\n) {\n  return (\n    previousLayout.slideCount === nextLayout.slideCount &&\n    previousLayout.slideTopPadding === nextLayout.slideTopPadding &&\n    previousLayout.slideGap === nextLayout.slideGap &&\n    previousLayout.slideHeight === nextLayout.slideHeight &&\n    previousLayout.slideWidth === nextLayout.slideWidth &&\n    previousLayout.slideStride === nextLayout.slideStride &&\n    previousLayout.totalHeight === nextLayout.totalHeight\n  );\n}\n\nfunction getPptxSlidesInRange({\n  layout,\n  startOffset,\n  endOffset,\n}: {\n  layout: PptxSlideLayout;\n  startOffset: number;\n  endOffset: number;\n}) {\n  const safeStartOffset = Math.max(0, finiteNumber(startOffset));\n  const safeEndOffset = Math.max(safeStartOffset, finiteNumber(endOffset));\n  const firstIndex = getPptxSlideIndexAtOffset(layout, safeStartOffset);\n  const lastIndex = getPptxSlideIndexAtOffset(layout, safeEndOffset);\n\n  return createPptxSlideRange(layout, firstIndex, lastIndex);\n}\n\nfunction createPptxSlideRange(\n  layout: PptxSlideLayout,\n  firstIndex: number,\n  lastIndex: number,\n) {\n  if (layout.slideCount === 0 || lastIndex < firstIndex) return [];\n\n  return Array.from(\n    { length: lastIndex - firstIndex + 1 },\n    (_, offset): PptxVirtualSlide => {\n      const index = firstIndex + offset;\n      const slideNumber = index + 1;\n      return {\n        height: layout.slideHeight,\n        index,\n        key: String(slideNumber),\n        slideNumber,\n        top: getPptxSlideTop(layout, index),\n        width: layout.slideWidth,\n      };\n    },\n  );\n}\n\nfunction getPptxSlideIndexAtOffset(layout: PptxSlideLayout, offset: number) {\n  if (layout.slideCount === 0 || layout.slideStride <= 0) return 0;\n\n  return clamp(\n    Math.floor(\n      (finiteNumber(offset) - layout.slideTopPadding) / layout.slideStride,\n    ),\n    0,\n    layout.slideCount - 1,\n  );\n}\n\nfunction shouldRebasePptxScroll({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return (\n    getPptxMaxLogicalScrollTop({ totalHeight, viewportHeight }) >\n    PPTX_SCROLL_REBASE_THRESHOLD_PX\n  );\n}\n\nfunction getPptxMaxLogicalScrollTop({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return Math.max(\n    safeSize(totalHeight) - Math.max(0, finiteNumber(viewportHeight)),\n    0,\n  );\n}\n\nfunction getPptxMaxPhysicalScrollTop({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return Math.max(\n    getPptxPhysicalScrollHeight({ totalHeight, viewportHeight }) -\n      Math.max(0, finiteNumber(viewportHeight)),\n    0,\n  );\n}\n\nfunction getPptxMaxScrollPageOffset({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return Math.max(\n    getPptxMaxLogicalScrollTop({ totalHeight, viewportHeight }) -\n      getPptxMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n    0,\n  );\n}\n\nfunction clampPptxScrollPageOffset({\n  scrollPageOffset,\n  totalHeight,\n  viewportHeight,\n}: {\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return clamp(\n    safePadding(scrollPageOffset),\n    0,\n    getPptxMaxScrollPageOffset({ totalHeight, viewportHeight }),\n  );\n}\n\nfunction resolvePptxScrollPageWindow({\n  logicalScrollTop,\n  preferredPhysicalScrollTop,\n  totalHeight,\n  viewportHeight,\n}: {\n  logicalScrollTop: number;\n  preferredPhysicalScrollTop: number;\n  totalHeight: number;\n  viewportHeight: number;\n}): PptxScrollRebasePosition {\n  let physicalScrollTop = roundPptxScrollPixel(\n    clamp(\n      finiteNumber(preferredPhysicalScrollTop),\n      0,\n      getPptxMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n    ),\n  );\n  let scrollPageOffset = clampPptxScrollPageOffset({\n    scrollPageOffset: logicalScrollTop - physicalScrollTop,\n    totalHeight,\n    viewportHeight,\n  });\n\n  physicalScrollTop = roundPptxScrollPixel(\n    clamp(\n      logicalScrollTop - scrollPageOffset,\n      0,\n      getPptxMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n    ),\n  );\n  scrollPageOffset = clampPptxScrollPageOffset({\n    scrollPageOffset: logicalScrollTop - physicalScrollTop,\n    totalHeight,\n    viewportHeight,\n  });\n\n  return { physicalScrollTop, scrollPageOffset };\n}\n\nfunction setViewportPhysicalScrollTop(\n  viewport: HTMLDivElement,\n  targetTop: number,\n) {\n  if (\n    Math.abs(viewport.scrollTop - targetTop) <= PPTX_SCROLL_POSITION_EPSILON\n  ) {\n    return;\n  }\n  viewport.scrollTop = targetTop;\n}\n\nfunction roundPptxScrollPixel(value: number) {\n  return Math.round(value);\n}\n\nfunction safeSize(value: number) {\n  return Math.max(0, finiteNumber(value));\n}\n\nfunction safePadding(value: number) {\n  return Math.max(0, finiteNumber(value));\n}\n\nfunction finiteNumber(value: number) {\n  return Number.isFinite(value) ? value : 0;\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-visible-slide.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-zoom-motion.ts",
      "content": "import { clamp } from \"./pptx-viewer-core\";\nimport type { ViewerDocumentZoomMotionController } from \"./viewer-types\";\n\n// Structural copy of PptxSlideLayout's zoom-relevant slice, so the visible-\n// slide module (which consumes this one) is not also an import of it.\nexport type PptxZoomSlideLayout = {\n  slideCount: number;\n  slideTopPadding: number;\n  slideHeight: number;\n  slideStride: number;\n  totalHeight: number;\n};\n\n// A toolbar zoom step re-anchors the viewport CENTER on both axes (Apple\n// Preview semantics): the content point under the viewport center before the\n// step is back under the viewport center after it, and a short FLIP relax\n// scales the painted surface about that fixed point. The reading-marker\n// restore (20% from the top, block axis only) stays the semantics for every\n// other geometry change — re-fits, rotation, container resizes — where the\n// intent is \"keep my reading position\", not \"zoom the camera\".\nconst PPTX_ZOOM_CENTER_MARKER_RATIO = 0.5;\nconst PPTX_ZOOM_MOTION_DURATION_MS = 200;\n// easeOutCubic: fast attack, gentle landing — a linear relax reads as a\n// hard stop at settle.\nconst PPTX_ZOOM_MOTION_EASING = \"cubic-bezier(0.33, 1, 0.68, 1)\";\nconst PPTX_ZOOM_MOTION_CLEANUP_MS = PPTX_ZOOM_MOTION_DURATION_MS + 50;\nconst PPTX_ZOOM_MOTION_MIN_TRANSLATE_PX = 0.5;\nconst PPTX_ZOOM_MOTION_MIN_SCALE_DELTA = 0.001;\n// A rebased (paged) physical scroll detaches the stage box from the content\n// scale, so the whole-surface FLIP would warp. Its signature is precise: the\n// rebased axis is PINNED (the container keeps its size while the content\n// rescales), so one axis barely moves while the other moves a lot. Testing for\n// that beats a plain ratio tolerance — an honest slide stack carries small\n// constant terms (rounded gaps, fixed outer padding) that put the block axis a\n// couple of percent off the inline one. On a big jump a tight ratio tolerance\n// then refuses the relax and the whole scale change lands in one frame —\n// measured on the image viewer as an un-animated 620px snap on a multi-frame\n// TIFF's fit-width, and this stack has the same shape of layout. The\n// wide ratio net below still catches anything wilder, and the FLIP writes\n// per-axis scales, so a slightly non-affine layout renders exactly.\nconst PPTX_ZOOM_MOTION_AXIS_FROZEN_DELTA = 0.02;\nconst PPTX_ZOOM_MOTION_AXIS_MOVED_DELTA = 0.05;\nconst PPTX_ZOOM_MOTION_AXIS_MISMATCH_RATIO = 0.25;\n\n// The exact wait-out of the relax before the visual clip re-tightens; clip\n// release is React-rendered state, so it outlives the inline transition.\nexport const PPTX_ZOOM_MOTION_TOTAL_MS = PPTX_ZOOM_MOTION_CLEANUP_MS + 50;\n\n// A zoom intent is consumed by the layout commit its gesture causes; if no\n// commit claims it in this window (scale already clamped, controlled scale\n// ignored by the owner), it is stale and must not re-anchor a later,\n// unrelated layout change.\nexport const PPTX_ZOOM_INTENT_MAX_AGE_MS = 400;\n\n// The document surface shrink-wraps the slide column (width = slideWidth,\n// exactly linear in scale) — the inline-anchor ruler. The FLIP relaxes on the\n// virtual canvas inside it: the canvas spans the full (physical) scroll\n// height and is NOT the kernel-registered surface, whose style the shell\n// motion owns.\nconst PPTX_ZOOM_STAGE_SELECTOR = '[data-slot=\"pptx-viewer-document-surface\"]';\nconst PPTX_ZOOM_VISUAL_LAYER_SELECTOR =\n  '[data-slot=\"pptx-slide-virtual-canvas\"]';\n\nexport type PptxZoomTransaction = {\n  slideNumber: number;\n  /**\n   * Deliberately unclamped, like the reading anchor: the slide layout is\n   * exactly linear in scale (fractional gap, fixed outer padding), so a\n   * center marker resting in a gap restores by the same slide-relative\n   * fraction.\n   */\n  yPercent: number;\n  /** Viewport-center position as a fraction of the stage's inline size. */\n  inlineFraction: number | null;\n  /**\n   * Same, on the BLOCK axis. Rect-derived like the inline one, so the solve\n   * cannot be thrown off by anything the layout model does not know about —\n   * chiefly the auto margins that centre a zoomed-out deck inside the pane.\n   * The slide model stays the fallback for a rebased (paged) scroll, where\n   * the stage stops spanning the deck.\n   */\n  blockFraction: number | null;\n  /** Painted visual-layer rect at click time — the FLIP's \"first\" frame. */\n  previousVisualRect: DOMRectReadOnly | null;\n};\n\nexport function createPptxZoomMotionController(\n  layout: PptxZoomSlideLayout,\n): ViewerDocumentZoomMotionController<PptxZoomTransaction> {\n  return {\n    capture: ({ scrollTop, viewportElement }) =>\n      capturePptxZoomTransaction({ layout, scrollTop, viewportElement }),\n    resolveScrollTarget: ({ transaction, viewportElement }) =>\n      resolvePptxZoomScrollTarget({ layout, transaction, viewportElement }),\n    play: ({ transaction, viewportElement }) =>\n      playPptxZoomMotion({ transaction, viewportElement }),\n  };\n}\n\nexport function capturePptxZoomTransaction({\n  layout,\n  scrollTop,\n  viewportElement,\n}: {\n  layout: PptxZoomSlideLayout;\n  /** LOGICAL scroll top — the caller maps out of the paged physical space. */\n  scrollTop: number;\n  viewportElement: HTMLDivElement;\n}): PptxZoomTransaction | null {\n  if (layout.slideCount === 0 || layout.slideHeight <= 0) return null;\n\n  const viewportBlockSize = Math.max(0, viewportElement.clientHeight);\n  const centerOffset =\n    Math.max(0, scrollTop) + viewportBlockSize * PPTX_ZOOM_CENTER_MARKER_RATIO;\n  const slideNumber = getPptxZoomSlideAtOffset(layout, centerOffset);\n  const slideTop = getPptxZoomSlideTop(layout, slideNumber - 1);\n\n  return {\n    slideNumber,\n    yPercent: (centerOffset - slideTop) / layout.slideHeight,\n    inlineFraction: capturePptxZoomInlineFraction(viewportElement),\n    blockFraction: capturePptxZoomBlockFraction(viewportElement),\n    previousVisualRect: readElementRect(\n      findPptxZoomVisualLayer(viewportElement),\n    ),\n  };\n}\n\nexport function resolvePptxZoomScrollTarget({\n  layout,\n  transaction,\n  viewportElement,\n}: {\n  layout: PptxZoomSlideLayout;\n  transaction: PptxZoomTransaction;\n  viewportElement: HTMLDivElement;\n}): { left?: number; top: number } | null {\n  if (\n    layout.slideHeight <= 0 ||\n    transaction.slideNumber < 1 ||\n    transaction.slideNumber > layout.slideCount\n  ) {\n    return null;\n  }\n\n  const viewportBlockSize = Math.max(0, viewportElement.clientHeight);\n  const maxScrollTop = Math.max(0, layout.totalHeight - viewportBlockSize);\n  const slideTop = getPptxZoomSlideTop(layout, transaction.slideNumber - 1);\n  // LOGICAL target — the caller maps back into the paged physical space.\n  const top = clamp(\n    resolvePptxZoomScrollTop({ layout, transaction, viewportElement }) ??\n      slideTop +\n        layout.slideHeight * transaction.yPercent -\n        viewportBlockSize * PPTX_ZOOM_CENTER_MARKER_RATIO,\n    0,\n    maxScrollTop,\n  );\n\n  const left = resolvePptxZoomScrollLeft(viewportElement, transaction);\n  return { top, ...(left == null ? null : { left }) };\n}\n\n// The stage is measured by live rects rather than re-deriving its centered\n// offset, so the math is direction-agnostic: RTL scrollLeft coordinate spaces\n// and the browser's own clamping both fall out for free.\nfunction capturePptxZoomInlineFraction(viewportElement: HTMLDivElement) {\n  const stageRect = readElementRect(findPptxZoomStage(viewportElement));\n  if (!stageRect || stageRect.width <= 0) return null;\n  return (\n    (getViewportCenterX(viewportElement) - stageRect.left) / stageRect.width\n  );\n}\n\nfunction capturePptxZoomBlockFraction(viewportElement: HTMLDivElement) {\n  const stageRect = readElementRect(findPptxZoomStage(viewportElement));\n  if (!stageRect || stageRect.height <= 0) return null;\n  return (\n    (getViewportCenterY(viewportElement) - stageRect.top) / stageRect.height\n  );\n}\n\n// Scroll down by however far the anchored content point currently sits below\n// the viewport centre — the block mirror of the inline solve. Only valid while\n// the stage box IS the deck; a rebased scroll detaches the two and the caller\n// falls back to the slide model.\nfunction resolvePptxZoomScrollTop({\n  layout,\n  transaction,\n  viewportElement,\n}: {\n  layout: PptxZoomSlideLayout;\n  transaction: PptxZoomTransaction;\n  viewportElement: HTMLDivElement;\n}) {\n  if (transaction.blockFraction == null) return null;\n  const stageRect = readElementRect(findPptxZoomStage(viewportElement));\n  if (!stageRect || stageRect.height <= 0) return null;\n  if (Math.abs(stageRect.height - layout.totalHeight) > 2) return null;\n  return (\n    viewportElement.scrollTop +\n    (stageRect.top + stageRect.height * transaction.blockFraction) -\n    getViewportCenterY(viewportElement)\n  );\n}\n\nfunction resolvePptxZoomScrollLeft(\n  viewportElement: HTMLDivElement,\n  transaction: PptxZoomTransaction,\n) {\n  if (transaction.inlineFraction == null) return undefined;\n  const stageRect = readElementRect(findPptxZoomStage(viewportElement));\n  if (!stageRect || stageRect.width <= 0) return undefined;\n\n  // Scroll right by however far the anchored content point currently sits\n  // right of the viewport center; the browser clamps to the scrollable range\n  // (which also zeroes it out when the stage fits without overflow).\n  return (\n    viewportElement.scrollLeft +\n    (stageRect.left + stageRect.width * transaction.inlineFraction) -\n    getViewportCenterX(viewportElement)\n  );\n}\n\nexport function playPptxZoomMotion({\n  transaction,\n  viewportElement,\n}: {\n  transaction: PptxZoomTransaction;\n  viewportElement: HTMLDivElement;\n}): (() => void) | null {\n  if (typeof requestAnimationFrame !== \"function\") return null;\n  if (prefersReducedMotion()) return null;\n\n  const previousRect = transaction.previousVisualRect;\n  const visualLayer = findPptxZoomVisualLayer(viewportElement);\n  if (!previousRect || !visualLayer) return null;\n\n  const currentRect = readElementRect(visualLayer);\n  if (\n    !currentRect ||\n    previousRect.width <= 0 ||\n    previousRect.height <= 0 ||\n    currentRect.width <= 0 ||\n    currentRect.height <= 0\n  ) {\n    return null;\n  }\n\n  const scaleX = previousRect.width / currentRect.width;\n  const scaleY = previousRect.height / currentRect.height;\n  if (hasDetachedPptxZoomAxes(scaleX, scaleY)) {\n    return null;\n  }\n\n  const translateX = previousRect.left - currentRect.left;\n  const translateY = previousRect.top - currentRect.top;\n  const hasVisibleDelta =\n    Math.abs(translateX) > PPTX_ZOOM_MOTION_MIN_TRANSLATE_PX ||\n    Math.abs(translateY) > PPTX_ZOOM_MOTION_MIN_TRANSLATE_PX ||\n    Math.abs(1 - scaleX) > PPTX_ZOOM_MOTION_MIN_SCALE_DELTA ||\n    Math.abs(1 - scaleY) > PPTX_ZOOM_MOTION_MIN_SCALE_DELTA;\n  if (!hasVisibleDelta) return null;\n\n  // Re-express the FLIP about the viewport center instead of the canvas's\n  // top-left: the canvas is the full deck (potentially hundreds of thousands\n  // of px tall), and a scale that far from its origin runs into GPU float\n  // precision. Anchoring at the viewport keeps the rasterized region's\n  // coordinates small; the mapping is identical.\n  const originX = getViewportCenterX(viewportElement) - currentRect.left;\n  const originY =\n    viewportElement.getBoundingClientRect().top +\n    Math.max(0, viewportElement.clientHeight) / 2 -\n    currentRect.top;\n  const anchoredTranslateX = translateX + (scaleX - 1) * originX;\n  const anchoredTranslateY = translateY + (scaleY - 1) * originY;\n\n  let cleanupTimeout: ReturnType<typeof setTimeout> | null = null;\n  let startFrame = 0;\n  let finished = false;\n  const finish = () => {\n    if (finished) return;\n    finished = true;\n    if (startFrame !== 0) cancelAnimationFrame(startFrame);\n    if (cleanupTimeout !== null) clearTimeout(cleanupTimeout);\n    removeInterruptListeners();\n    visualLayer.style.transition = \"\";\n    visualLayer.style.transform = \"\";\n    visualLayer.style.transformOrigin = \"\";\n    visualLayer.style.willChange = \"\";\n  };\n  // A user gesture mid-relax snaps to the committed endpoint: layout and\n  // scroll already landed in the zoom's own commit, so clearing the transform\n  // is always safe — and content must never keep gliding under a live scroll.\n  // (Gesture events only; the zoom's own programmatic scroll writes do not\n  // fire these.)\n  const removeInterruptListeners = attachPptxZoomInterruptListeners(\n    viewportElement,\n    () => finish(),\n  );\n\n  visualLayer.style.transition = \"none\";\n  visualLayer.style.transformOrigin = `${originX}px ${originY}px`;\n  visualLayer.style.transform = `translate3d(${anchoredTranslateX}px, ${anchoredTranslateY}px, 0px) scale(${scaleX}, ${scaleY})`;\n  visualLayer.style.willChange = \"transform\";\n\n  startFrame = requestAnimationFrame(() => {\n    startFrame = 0;\n    if (finished) return;\n    visualLayer.style.transition = `transform ${PPTX_ZOOM_MOTION_DURATION_MS}ms ${PPTX_ZOOM_MOTION_EASING}`;\n    visualLayer.style.transform = \"translate3d(0px, 0px, 0px) scale(1, 1)\";\n  });\n  cleanupTimeout = setTimeout(finish, PPTX_ZOOM_MOTION_CLEANUP_MS);\n\n  return finish;\n}\n\nfunction attachPptxZoomInterruptListeners(\n  viewportElement: HTMLDivElement,\n  interrupt: () => void,\n) {\n  if (typeof viewportElement.addEventListener !== \"function\") return () => {};\n  const events = [\"wheel\", \"touchstart\", \"pointerdown\", \"keydown\"] as const;\n  for (const event of events) {\n    viewportElement.addEventListener(event, interrupt, { passive: true });\n  }\n  return () => {\n    for (const event of events) {\n      viewportElement.removeEventListener(event, interrupt);\n    }\n  };\n}\n\nfunction getPptxZoomSlideAtOffset(layout: PptxZoomSlideLayout, offset: number) {\n  if (layout.slideCount <= 1 || layout.slideStride <= 0) return 1;\n  const slideIndex = Math.floor(\n    (offset - layout.slideTopPadding) / layout.slideStride,\n  );\n  return clamp(slideIndex + 1, 1, layout.slideCount);\n}\n\nfunction getPptxZoomSlideTop(layout: PptxZoomSlideLayout, slideIndex: number) {\n  return layout.slideTopPadding + slideIndex * layout.slideStride;\n}\n\nfunction findPptxZoomStage(viewportElement: HTMLDivElement) {\n  if (typeof viewportElement.querySelector !== \"function\") return null;\n  return viewportElement.querySelector<HTMLElement>(PPTX_ZOOM_STAGE_SELECTOR);\n}\n\nfunction findPptxZoomVisualLayer(viewportElement: HTMLDivElement) {\n  if (typeof viewportElement.querySelector !== \"function\") return null;\n  return viewportElement.querySelector<HTMLElement>(\n    PPTX_ZOOM_VISUAL_LAYER_SELECTOR,\n  );\n}\n\nfunction getViewportCenterX(viewportElement: HTMLDivElement) {\n  return (\n    viewportElement.getBoundingClientRect().left +\n    Math.max(0, viewportElement.clientWidth) / 2\n  );\n}\n\nfunction getViewportCenterY(viewportElement: HTMLDivElement) {\n  return (\n    viewportElement.getBoundingClientRect().top +\n    Math.max(0, viewportElement.clientHeight) / 2\n  );\n}\n\n// True when one axis is pinned while the other rescales — the paged-scroll\n// signature — or when the two ratios are so far apart that the stage box\n// cannot be tracking the content at all.\nfunction hasDetachedPptxZoomAxes(scaleX: number, scaleY: number) {\n  const inlineDelta = Math.abs(scaleX - 1);\n  const blockDelta = Math.abs(scaleY - 1);\n  const frozenAxis =\n    (blockDelta < PPTX_ZOOM_MOTION_AXIS_FROZEN_DELTA &&\n      inlineDelta > PPTX_ZOOM_MOTION_AXIS_MOVED_DELTA) ||\n    (inlineDelta < PPTX_ZOOM_MOTION_AXIS_FROZEN_DELTA &&\n      blockDelta > PPTX_ZOOM_MOTION_AXIS_MOVED_DELTA);\n  return (\n    frozenAxis ||\n    Math.abs(scaleX - scaleY) >\n      PPTX_ZOOM_MOTION_AXIS_MISMATCH_RATIO * Math.max(scaleX, scaleY)\n  );\n}\n\nfunction readElementRect(element: HTMLElement | null) {\n  if (!element || typeof element.getBoundingClientRect !== \"function\") {\n    return null;\n  }\n  const rect = element.getBoundingClientRect();\n  return rect.width > 0 && rect.height > 0 ? rect : null;\n}\n\nfunction prefersReducedMotion() {\n  return (\n    typeof matchMedia === \"function\" &&\n    matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-zoom-motion.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-zoom.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { normalizePptxScale } from \"./pptx-viewer-core\";\n\ntype PptxZoomState =\n  | { mode: \"fit\" }\n  | {\n      mode: \"manual\";\n      value: number;\n    };\n\nexport interface PptxZoomInput {\n  controlledScale?: number;\n  defaultScale?: number;\n  fitScale: number;\n  onScaleChange?: (scale: number | null) => void;\n}\n\nexport function usePptxZoom({\n  controlledScale,\n  defaultScale,\n  fitScale,\n  onScaleChange,\n}: PptxZoomInput) {\n  const [zoomState, setZoomState] = React.useState<PptxZoomState>(() =>\n    defaultScale == null\n      ? { mode: \"fit\" }\n      : { mode: \"manual\", value: normalizePptxScale(defaultScale) },\n  );\n\n  const isScaleControlled = controlledScale !== undefined;\n  const normalizedControlledScale = isScaleControlled\n    ? normalizePptxScale(controlledScale)\n    : undefined;\n  const zoomScale =\n    normalizedControlledScale ??\n    (zoomState.mode === \"manual\" ? zoomState.value : fitScale);\n  const isFitWidth = !isScaleControlled && zoomState.mode === \"fit\";\n  const scaleControlsDisabled = isScaleControlled && !onScaleChange;\n\n  const setViewerScale = React.useCallback(\n    (nextScale: number | null) => {\n      const normalized =\n        nextScale == null ? null : normalizePptxScale(nextScale);\n      if (isScaleControlled) {\n        onScaleChange?.(normalized);\n        return;\n      }\n      setZoomState(\n        normalized == null\n          ? { mode: \"fit\" }\n          : { mode: \"manual\", value: normalized },\n      );\n    },\n    [isScaleControlled, onScaleChange],\n  );\n\n  return { isFitWidth, scaleControlsDisabled, setViewerScale, zoomScale };\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-zoom.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-source.ts",
      "content": "import type {\n  ViewerContentBytes,\n  ViewerContentIdentity,\n} from \"@/lib/viewer-resource\";\n\nimport {\n  DisposableLruCache,\n  PptxBitmapEntry,\n  type Disposable,\n} from \"./pptx-viewer-cache\";\nimport {\n  getPptxBitmapCacheKey,\n  type PptxBitmapCacheInput,\n  type PptxSize,\n  type PptxSlideRenderPriority,\n  type PptxSourceLoadTiming,\n} from \"./pptx-viewer-core\";\nimport {\n  createPptxRenderer,\n  PptxRendererError,\n  type PptxRenderer,\n} from \"./pptx-viewer-renderer\";\n\nconst PPTX_SOURCE_CACHE_MAX = 4;\nconst PPTX_SOURCE_TIMING_CACHE_MAX = 32;\nconst PPTX_BITMAP_CACHE_MAX = 8;\nconst PPTX_BITMAP_CACHE_MAX_PIXELS = 24_000_000;\n\nexport type PptxSourceRelease = () => void;\n\nexport interface PptxSourceRenderInput extends PptxBitmapCacheInput {\n  canvas: HTMLCanvasElement;\n  isLive?: () => boolean;\n  priority?: PptxSlideRenderPriority;\n}\n\nexport interface PptxSourceDrawCachedBitmapInput extends PptxBitmapCacheInput {\n  canvas: HTMLCanvasElement;\n}\n\nexport type PptxRenderResult =\n  | { status: \"rendered\" }\n  | { status: \"cancelled\" }\n  | { status: \"failed\"; error: PptxRendererError };\n\nexport interface PptxSource extends Disposable {\n  slideCount: number;\n  baseSize: PptxSize;\n  drawCachedBitmap(\n    input: PptxSourceDrawCachedBitmapInput,\n  ): PptxRenderResult | null;\n  renderSlide(input: PptxSourceRenderInput): Promise<PptxRenderResult>;\n  hasBitmap(input: PptxBitmapCacheInput): boolean;\n  retain(): PptxSourceRelease;\n}\n\nclass RendererSource implements PptxSource {\n  readonly slideCount: number;\n  readonly baseSize: PptxSize;\n\n  private readonly bitmaps = new DisposableLruCache<string, PptxBitmapEntry>(\n    PPTX_BITMAP_CACHE_MAX,\n    {\n      maxCost: PPTX_BITMAP_CACHE_MAX_PIXELS,\n      getCost: (entry) => entry.pixelCount,\n    },\n  );\n  private queue: PptxQueuedRender[] = [];\n  private isRendering = false;\n  private nextSequence = 1;\n  private bitmapSnapshots = new Map<string, Promise<void>>();\n  private retainCount = 0;\n  private disposeRequested = false;\n  private disposed = false;\n\n  constructor(private readonly renderer: PptxRenderer) {\n    this.slideCount = renderer.slideCount;\n    this.baseSize = renderer.baseSize;\n  }\n\n  hasBitmap(input: PptxBitmapCacheInput) {\n    if (this.disposed) return false;\n    return this.bitmaps.get(getPptxBitmapCacheKey(input)) !== undefined;\n  }\n\n  drawCachedBitmap(\n    input: PptxSourceDrawCachedBitmapInput,\n  ): PptxRenderResult | null {\n    if (this.disposed) return null;\n    const cached = this.bitmaps.get(getPptxBitmapCacheKey(input));\n    if (!cached) return null;\n    return drawPptxBitmap(input.canvas, cached.bitmap);\n  }\n\n  renderSlide(input: PptxSourceRenderInput): Promise<PptxRenderResult> {\n    if (this.disposed) {\n      return Promise.resolve({\n        status: \"failed\",\n        error: new PptxRendererError(\n          \"disposed\",\n          \"Presentation source was disposed.\",\n        ),\n      });\n    }\n    if (!isValidSlideIndex(input.slideIndex, this.slideCount)) {\n      return Promise.resolve({\n        status: \"failed\",\n        error: new PptxRendererError(\n          \"index_out_of_range\",\n          `Slide ${input.slideIndex + 1} is outside the presentation.`,\n        ),\n      });\n    }\n    if (!isValidRenderScale(input.renderScale)) {\n      return Promise.resolve({\n        status: \"failed\",\n        error: new PptxRendererError(\n          \"bounds\",\n          \"Render scale must be a positive finite number.\",\n        ),\n      });\n    }\n\n    const bitmapKey = getPptxBitmapCacheKey(input);\n    if (this.hasBitmap(input)) {\n      if (!isRenderLive(input)) return Promise.resolve({ status: \"cancelled\" });\n      const cached = this.drawCachedBitmap(input);\n      if (cached) return Promise.resolve(cached);\n    }\n\n    const snapshot = this.bitmapSnapshots.get(bitmapKey);\n    if (snapshot) return this.renderAfterSnapshot(input, snapshot);\n\n    return new Promise<PptxRenderResult>((resolve) => {\n      this.queue.push({\n        bitmapKey,\n        input,\n        resolve,\n        sequence: this.nextSequence,\n      });\n      this.nextSequence += 1;\n      this.pumpQueue();\n    });\n  }\n\n  retain(): PptxSourceRelease {\n    if (this.disposed) return () => {};\n    this.retainCount += 1;\n    let hasReleased = false;\n    return () => {\n      if (hasReleased) return;\n      hasReleased = true;\n      this.retainCount -= 1;\n      if (this.retainCount === 0 && this.disposeRequested) this.close();\n    };\n  }\n\n  dispose() {\n    this.disposeRequested = true;\n    if (this.retainCount === 0) this.close();\n  }\n\n  private close() {\n    if (this.disposed) return;\n    this.disposed = true;\n    for (const task of this.queue.splice(0))\n      task.resolve({ status: \"cancelled\" });\n    this.bitmapSnapshots.clear();\n    this.bitmaps.clear();\n    this.renderer.dispose();\n  }\n\n  private pumpQueue() {\n    if (this.isRendering) return;\n    this.isRendering = true;\n    void this.drainQueue();\n  }\n\n  private async drainQueue() {\n    try {\n      while (!this.disposed && this.queue.length > 0) {\n        const task = this.takeNextTask();\n        if (!task) break;\n        await this.runTask(task);\n      }\n    } finally {\n      this.isRendering = false;\n      if (!this.disposed && this.queue.length > 0) this.pumpQueue();\n    }\n  }\n\n  private takeNextTask() {\n    this.pruneStaleQueuedTasks();\n\n    let bestIndex = -1;\n    let bestRank: PptxRenderRank | null = null;\n    for (let index = 0; index < this.queue.length; index += 1) {\n      const task = this.queue[index];\n      if (!task) continue;\n      const rank = getRenderRank(task);\n      if (!bestRank || compareRenderRanks(rank, bestRank) < 0) {\n        bestRank = rank;\n        bestIndex = index;\n      }\n    }\n    if (bestIndex < 0) return null;\n    const [task] = this.queue.splice(bestIndex, 1);\n    return task ?? null;\n  }\n\n  private pruneStaleQueuedTasks() {\n    const liveQueue: PptxQueuedRender[] = [];\n    for (const task of this.queue) {\n      if (isRenderLive(task.input)) {\n        liveQueue.push(task);\n      } else {\n        task.resolve({ status: \"cancelled\" });\n      }\n    }\n    this.queue = liveQueue;\n  }\n\n  private async runTask(task: PptxQueuedRender) {\n    if (this.disposed) {\n      task.resolve({ status: \"cancelled\" });\n      return;\n    }\n    if (!isRenderLive(task.input)) {\n      task.resolve({ status: \"cancelled\" });\n      return;\n    }\n\n    const cached = this.drawCachedBitmap(task.input);\n    if (cached) {\n      task.resolve(cached);\n      return;\n    }\n\n    const pendingSnapshot = this.bitmapSnapshots.get(task.bitmapKey);\n    if (pendingSnapshot) {\n      task.resolve(this.renderAfterSnapshot(task.input, pendingSnapshot));\n      return;\n    }\n\n    try {\n      await this.renderer.renderSlide(task.input);\n    } catch (error) {\n      if (this.disposed || !isRenderLive(task.input)) {\n        task.resolve({ status: \"cancelled\" });\n        return;\n      }\n      task.resolve({\n        status: \"failed\",\n        error: normalizeRendererError(error),\n      });\n      return;\n    }\n\n    if (this.disposed || !isRenderLive(task.input)) {\n      task.resolve({ status: \"cancelled\" });\n      return;\n    }\n\n    const snapshot = this.scheduleBitmapSnapshot(task);\n    this.resolveQueuedBitmapDuplicates(task.bitmapKey, snapshot);\n    task.resolve({ status: \"rendered\" });\n  }\n\n  private renderAfterSnapshot(\n    input: PptxSourceRenderInput,\n    snapshot: Promise<void>,\n  ): Promise<PptxRenderResult> {\n    return snapshot.then(\n      () => {\n        if (this.disposed) return { status: \"cancelled\" };\n        if (!isRenderLive(input)) return { status: \"cancelled\" };\n        const cached = this.drawCachedBitmap(input);\n        if (cached) return cached;\n        return this.renderSlide(input);\n      },\n      () => {\n        if (this.disposed) return { status: \"cancelled\" };\n        if (!isRenderLive(input)) return { status: \"cancelled\" };\n        return this.renderSlide(input);\n      },\n    );\n  }\n\n  private scheduleBitmapSnapshot(task: PptxQueuedRender) {\n    const snapshot = this.captureBitmap(task);\n    this.bitmapSnapshots.set(task.bitmapKey, snapshot);\n    snapshot.finally(() => {\n      if (this.bitmapSnapshots.get(task.bitmapKey) === snapshot) {\n        this.bitmapSnapshots.delete(task.bitmapKey);\n      }\n    });\n    return snapshot;\n  }\n\n  private resolveQueuedBitmapDuplicates(\n    bitmapKey: string,\n    snapshot: Promise<void>,\n  ) {\n    const remainingQueue: PptxQueuedRender[] = [];\n    for (const queued of this.queue) {\n      if (queued.bitmapKey !== bitmapKey) {\n        remainingQueue.push(queued);\n      } else if (isRenderLive(queued.input)) {\n        queued.resolve(this.renderAfterSnapshot(queued.input, snapshot));\n      } else {\n        queued.resolve({ status: \"cancelled\" });\n      }\n    }\n    this.queue = remainingQueue;\n  }\n\n  private async captureBitmap(task: PptxQueuedRender) {\n    let bitmap: ImageBitmap | null = null;\n    try {\n      bitmap = await createImageBitmap(task.input.canvas);\n      if (this.disposed || !isRenderLive(task.input)) {\n        bitmap.close();\n        return;\n      }\n      this.bitmaps.set(task.bitmapKey, new PptxBitmapEntry(bitmap));\n      bitmap = null;\n    } catch {\n      if (bitmap) bitmap.close();\n      /* Snapshot unsupported: the slide still rendered, just without cache. */\n    }\n  }\n}\n\ntype PptxQueuedRender = {\n  bitmapKey: string;\n  input: PptxSourceRenderInput;\n  resolve: (result: PptxRenderResult | PromiseLike<PptxRenderResult>) => void;\n  sequence: number;\n};\n\ntype PptxRenderRank = {\n  visibility: number;\n  distance: number;\n  sequence: number;\n};\n\nfunction getRenderRank(task: PptxQueuedRender): PptxRenderRank {\n  const priority = task.input.priority;\n  return {\n    visibility: priority?.isCurrentSlide\n      ? 0\n      : priority?.isInViewport\n        ? 1\n        : priority?.isScrollLead\n          ? 2\n          : 3,\n    distance:\n      priority && Number.isFinite(priority.distanceFromReadingMarker)\n        ? Math.max(0, priority.distanceFromReadingMarker)\n        : Number.MAX_SAFE_INTEGER,\n    sequence: task.sequence,\n  };\n}\n\nfunction compareRenderRanks(a: PptxRenderRank, b: PptxRenderRank) {\n  if (a.visibility !== b.visibility) return a.visibility - b.visibility;\n  if (a.distance !== b.distance) return a.distance - b.distance;\n  return a.sequence - b.sequence;\n}\n\nclass SourceCacheEntry implements Disposable {\n  source?: PptxSource;\n  loadTiming?: PptxSourceLoadTiming;\n  disposed = false;\n  private settled = false;\n  private readonly loadTimingSubscribers = new Set<\n    (timing: PptxSourceLoadTiming) => void\n  >();\n\n  constructor(readonly promise: Promise<PptxSource>) {\n    promise.then(\n      (source) => {\n        this.settled = true;\n        this.source = source;\n        if (this.disposed) source.dispose();\n      },\n      () => {\n        this.settled = true;\n        /* rejected entries are removed by getPptxSource */\n      },\n    );\n  }\n\n  /**\n   * A still-loading entry must not be evicted: dispose() cannot act on a source\n   * that has not resolved yet, so an evicted-pending entry would leak its\n   * renderer once the load completes (and a Suspense retry would create a\n   * duplicate). Pin it until the load settles.\n   */\n  isEvictable() {\n    return this.settled || this.disposed;\n  }\n\n  dispose() {\n    this.loadTimingSubscribers.clear();\n    this.disposed = true;\n    this.source?.dispose();\n  }\n\n  disposeWhenResolved() {\n    this.disposed = true;\n    this.loadTimingSubscribers.clear();\n    this.source?.dispose();\n  }\n\n  setLoadTiming(timing: PptxSourceLoadTiming) {\n    this.loadTiming = timing;\n    for (const subscriber of this.loadTimingSubscribers) {\n      notifyLoadTimingSubscriber(subscriber, timing);\n    }\n  }\n\n  subscribeLoadTiming(callback: (timing: PptxSourceLoadTiming) => void) {\n    this.loadTimingSubscribers.add(callback);\n    if (this.loadTiming) {\n      const loadTiming = this.loadTiming;\n      setTimeout(() => {\n        if (this.loadTimingSubscribers.has(callback)) {\n          notifyLoadTimingSubscriber(callback, loadTiming);\n        }\n      }, 0);\n    }\n    return () => {\n      this.loadTimingSubscribers.delete(callback);\n    };\n  }\n}\n\nfunction notifyLoadTimingSubscriber(\n  subscriber: (timing: PptxSourceLoadTiming) => void,\n  timing: PptxSourceLoadTiming,\n) {\n  try {\n    subscriber(timing);\n  } catch {\n    /* Instrumentation callbacks must not affect viewer loading. */\n  }\n}\n\nconst sourceCache = new DisposableLruCache<string, SourceCacheEntry>(\n  PPTX_SOURCE_CACHE_MAX,\n);\nconst sourceLoadTimingCache = new Map<string, PptxSourceLoadTiming>();\n\nexport function getPptxSource(\n  content: ViewerContentBytes,\n): Promise<PptxSource> {\n  const loadKey = content.key;\n  const cached = sourceCache.get(loadKey);\n  if (cached) return cached.promise;\n\n  sourceLoadTimingCache.delete(loadKey);\n\n  const pendingEntry: { current?: SourceCacheEntry } = {};\n  let pendingLoadTiming: PptxSourceLoadTiming | null = null;\n  const handleLoadTiming = (timing: PptxSourceLoadTiming) => {\n    rememberSourceLoadTiming(loadKey, timing);\n    if (pendingEntry.current) {\n      pendingEntry.current.setLoadTiming(timing);\n    } else {\n      pendingLoadTiming = timing;\n    }\n  };\n  const promise = createPptxRenderer(content, handleLoadTiming).then(\n    (renderer) => new RendererSource(renderer),\n    (error) => {\n      scheduleFailedSourceEviction(loadKey, pendingEntry.current);\n      throw error;\n    },\n  );\n  const entry = new SourceCacheEntry(promise);\n  pendingEntry.current = entry;\n  if (pendingLoadTiming) entry.setLoadTiming(pendingLoadTiming);\n  sourceCache.set(loadKey, entry);\n  return entry.promise;\n}\n\nexport function subscribePptxSourceLoadTiming(\n  content: ViewerContentIdentity,\n  callback: (timing: PptxSourceLoadTiming) => void,\n) {\n  const loadKey = content.key;\n  const entry = sourceCache.get(loadKey);\n  if (!entry) return subscribeCachedSourceLoadTiming(loadKey, callback);\n  return entry.subscribeLoadTiming(callback);\n}\n\nexport function evictPptxSource(content: ViewerContentIdentity) {\n  sourceLoadTimingCache.delete(content.key);\n  sourceCache.delete(content.key);\n}\n\nfunction subscribeCachedSourceLoadTiming(\n  loadKey: string,\n  callback: (timing: PptxSourceLoadTiming) => void,\n) {\n  const loadTiming = sourceLoadTimingCache.get(loadKey);\n  if (!loadTiming) return () => {};\n  let isSubscribed = true;\n  setTimeout(() => {\n    if (isSubscribed) notifyLoadTimingSubscriber(callback, loadTiming);\n  }, 0);\n  return () => {\n    isSubscribed = false;\n  };\n}\n\nfunction rememberSourceLoadTiming(\n  loadKey: string,\n  timing: PptxSourceLoadTiming,\n) {\n  sourceLoadTimingCache.delete(loadKey);\n  sourceLoadTimingCache.set(loadKey, timing);\n  while (sourceLoadTimingCache.size > PPTX_SOURCE_TIMING_CACHE_MAX) {\n    const oldestLoadKey = sourceLoadTimingCache.keys().next().value;\n    if (!oldestLoadKey) return;\n    sourceLoadTimingCache.delete(oldestLoadKey);\n  }\n}\n\nfunction scheduleFailedSourceEviction(\n  loadKey: string,\n  entry: SourceCacheEntry | undefined,\n) {\n  if (!entry) return;\n  setTimeout(() => {\n    if (sourceCache.get(loadKey) === entry) sourceCache.delete(loadKey);\n  }, 0);\n}\n\nexport function disposePptxSourceCache() {\n  for (const entry of sourceCache.snapshotValues()) {\n    entry.disposeWhenResolved();\n  }\n  sourceCache.clear();\n  sourceLoadTimingCache.clear();\n}\n\nfunction isRenderLive({ isLive }: Pick<PptxSourceRenderInput, \"isLive\">) {\n  try {\n    return !isLive || isLive();\n  } catch {\n    return false;\n  }\n}\n\nfunction isValidSlideIndex(slideIndex: number, slideCount: number) {\n  return (\n    Number.isInteger(slideIndex) && slideIndex >= 0 && slideIndex < slideCount\n  );\n}\n\nfunction isValidRenderScale(renderScale: number) {\n  return Number.isFinite(renderScale) && renderScale > 0;\n}\n\nfunction drawPptxBitmap(\n  canvas: HTMLCanvasElement,\n  bitmap: ImageBitmap,\n): PptxRenderResult {\n  try {\n    drawBitmap(canvas, bitmap);\n    return { status: \"rendered\" };\n  } catch (error) {\n    return {\n      status: \"failed\",\n      error: new PptxRendererError(\n        \"render_failed\",\n        \"Failed to draw cached slide bitmap.\",\n        error,\n      ),\n    };\n  }\n}\n\nfunction drawBitmap(canvas: HTMLCanvasElement, bitmap: ImageBitmap) {\n  canvas.width = bitmap.width;\n  canvas.height = bitmap.height;\n  const context = canvas.getContext(\"2d\");\n  if (!context) throw new Error(\"Canvas 2D context is unavailable.\");\n  context.drawImage(bitmap, 0, 0);\n}\n\nfunction normalizeRendererError(error: unknown) {\n  if (error instanceof PptxRendererError) return error;\n  return new PptxRendererError(\n    \"render_failed\",\n    \"Failed to render slide.\",\n    error,\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-source.ts"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-slide.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nimport {\n  getPptxRenderPixelRatio,\n  getScaledSlideSize,\n  getVisibleSlideSize,\n  type PptxSize,\n  type PptxSlideOverlayProps,\n  type PptxSlideRenderPriority,\n  type PptxSlideRenderTiming,\n} from \"./pptx-viewer-core\";\nimport { type PptxScrollActivity } from \"./pptx-viewer-scroll\";\nimport { type PptxSource } from \"./pptx-viewer-source\";\nimport {\n  createPptxSlideLayout,\n  getPptxRenderedSlideWindow,\n  getPptxRenderSlides,\n  getPptxSlideTop,\n  readPptxScrollMetrics,\n  PPTX_READING_MARKER_RATIO,\n  PPTX_RENDER_WINDOW_OVERSCAN_PX,\n  type PptxSlideLayout,\n  type PptxScrollMetrics,\n  type PptxVirtualSlide,\n} from \"./pptx-viewer-visible-slide\";\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\ntype SlideRenderState = \"idle\" | \"rendering\" | \"rendered\" | \"failed\";\ntype PptxScrollDirection = -1 | 0 | 1;\n\nexport const PPTX_SLIDE_GAP = 16;\nexport const PPTX_SLIDE_PADDING = 16;\nconst PPTX_TRANSITION_WINDOW_RELEASE_MS = 260;\nconst PPTX_UPWARD_SCROLL_LEAD_PX = PPTX_RENDER_WINDOW_OVERSCAN_PX;\n\nexport interface PptxSlideScrollerProps {\n  source: PptxSource;\n  zoomScale: number;\n  rotation: number;\n  layout?: PptxSlideLayout;\n  eager: boolean;\n  activity: PptxScrollActivity;\n  renderSlideOverlay?: (props: PptxSlideOverlayProps) => React.ReactNode;\n  onSlideRenderTiming?: (timing: PptxSlideRenderTiming) => void;\n  containerRef: React.Ref<HTMLDivElement>;\n  documentSurfaceRef?: React.Ref<HTMLDivElement>;\n  viewportRef: React.Ref<HTMLDivElement>;\n  getScrollMetrics?: () => PptxScrollMetrics;\n  /** The slide column is at its fit-width scale (the shell motion's domain). */\n  isFitWidth?: boolean;\n  isTransitioning?: boolean;\n  onScroll: () => void;\n}\n\nexport function PptxSlideScroller({\n  source,\n  zoomScale,\n  rotation,\n  layout: providedLayout,\n  eager,\n  activity,\n  renderSlideOverlay,\n  onSlideRenderTiming,\n  containerRef,\n  documentSurfaceRef,\n  viewportRef,\n  getScrollMetrics,\n  isFitWidth = true,\n  isTransitioning = false,\n  onScroll,\n}: PptxSlideScrollerProps) {\n  const layout = React.useMemo(\n    () =>\n      providedLayout ??\n      createPptxSlideLayout({\n        baseSize: source.baseSize,\n        zoomScale,\n        rotation,\n        slideCount: source.slideCount,\n        slideGap: PPTX_SLIDE_GAP,\n        slidePadding: PPTX_SLIDE_PADDING,\n      }),\n    [providedLayout, rotation, source.baseSize, source.slideCount, zoomScale],\n  );\n  const canvasRef = React.useRef<HTMLDivElement | null>(null);\n  const projectionFrameRef = React.useRef<number | null>(null);\n  const projectionCacheRef = React.useRef<PptxSlideProjectionCache>({\n    canvas: null,\n    hasMeasuredScroll: false,\n    lastScrollDirection: 0,\n    lastScrollTop: 0,\n    resetKey: \"\",\n    slides: new Map(),\n    window: null,\n  });\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null);\n  const projectSlidesRef = React.useRef<() => void>(() => {});\n  const isTransitioningRef = React.useRef(isTransitioning);\n  const transitionWindowReleaseTimerRef = React.useRef<number | null>(null);\n  isTransitioningRef.current = isTransitioning;\n  const layoutResetKey = `${layout.slideCount}:${layout.slideWidth}:${layout.slideHeight}:${layout.slideStride}:${layout.totalHeight}:${zoomScale}:${rotation}`;\n\n  const projectSlides = React.useCallback(() => {\n    projectionFrameRef.current = null;\n    const result = projectPptxSlides({\n      activity,\n      cache: projectionCacheRef.current,\n      canvas: canvasRef.current,\n      eager,\n      getScrollMetrics,\n      isTransitioning: isTransitioningRef.current,\n      layout,\n      onSlideRenderTiming,\n      renderSlideOverlay,\n      resetKey: layoutResetKey,\n      rotation,\n      source,\n      viewport: viewportElementRef.current,\n      zoomScale,\n    });\n    if (\n      result.fillOverscanNextFrame &&\n      projectionFrameRef.current === null &&\n      typeof requestAnimationFrame === \"function\"\n    ) {\n      projectionFrameRef.current = requestAnimationFrame(() =>\n        projectSlidesRef.current(),\n      );\n    }\n  }, [\n    activity,\n    eager,\n    getScrollMetrics,\n    layout,\n    layoutResetKey,\n    onSlideRenderTiming,\n    renderSlideOverlay,\n    rotation,\n    source,\n    zoomScale,\n  ]);\n  projectSlidesRef.current = projectSlides;\n\n  const scheduleProjectSlides = React.useCallback(() => {\n    if (projectionFrameRef.current !== null) return;\n    if (typeof requestAnimationFrame !== \"function\") {\n      projectSlides();\n      return;\n    }\n    projectionFrameRef.current = requestAnimationFrame(projectSlides);\n  }, [projectSlides]);\n\n  useKeyedLayoutEffect(joinEffectKey([projectSlides]), () => {\n    projectSlides();\n  });\n\n  const clearTransitionWindowReleaseTimer = React.useCallback(() => {\n    if (transitionWindowReleaseTimerRef.current === null) return;\n    window.clearTimeout(transitionWindowReleaseTimerRef.current);\n    transitionWindowReleaseTimerRef.current = null;\n  }, []);\n\n  // The sidebar slide freezes the visible-slide window (see projectPptxSlides).\n  // Once the motion settles — scroll rebased to the re-fit layout — re-derive the\n  // window after the sampled motion window, so cleanup never reads as motion\n  // churn.\n  useKeyedLayoutEffect(\n    joinEffectKey([\"pptx-transition\", isTransitioning]),\n    () => {\n      clearTransitionWindowReleaseTimer();\n      if (isTransitioning) return;\n\n      transitionWindowReleaseTimerRef.current = window.setTimeout(() => {\n        transitionWindowReleaseTimerRef.current = null;\n        projectSlidesRef.current();\n      }, PPTX_TRANSITION_WINDOW_RELEASE_MS);\n    },\n  );\n\n  useMountEffect(() => () => {\n    if (\n      projectionFrameRef.current !== null &&\n      typeof cancelAnimationFrame === \"function\"\n    ) {\n      cancelAnimationFrame(projectionFrameRef.current);\n    }\n    clearTransitionWindowReleaseTimer();\n    disposePptxSlideProjectionCache(projectionCacheRef.current);\n  });\n\n  const setViewportRef = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      viewportElementRef.current = element;\n      assignPptxRef(viewportRef, element);\n      assignPptxRef(containerRef, element);\n    },\n    [containerRef, viewportRef],\n  );\n\n  const setCanvasRef = React.useCallback((element: HTMLDivElement | null) => {\n    canvasRef.current = element;\n  }, []);\n\n  const handleScroll = React.useCallback(() => {\n    onScroll();\n    scheduleProjectSlides();\n  }, [onScroll, scheduleProjectSlides]);\n\n  return (\n    <div className=\"size-full min-h-0 flex-1\">\n      <div\n        ref={setViewportRef}\n        className=\"focus-visible:ring-ring focus-visible:ring-offset-background h-full overflow-auto rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-offset-1\"\n        data-slot=\"scroll-area-viewport\"\n        onScroll={handleScroll}\n        style={{ overflowAnchor: \"none\" }}\n      >\n        {/* The clip exists for ONE state: a fit-width shell slide, where the\n            kernel's counter-transform paints the surface past its committed\n            box, and that visual overflow would otherwise inflate the\n            scroller's scrollHeight and drag a max-clamped scroll position\n            down frame by frame as the transform relaxes. Every other state\n            must NOT clip: a zoomed-in surface's inline overflow IS the\n            horizontal scroll range (an unconditional clip froze scrollWidth\n            at the viewport width and made zoomed decks horizontally\n            unscrollable), and a zoom relax's enlarged opening frame must not\n            be cut at the committed box. At fit-width the surface fits the\n            layout width, so the active clip can never eat scrollable\n            overflow. */}\n        {/* Flex column so the surface's block-axis auto margin has free space\n            to split once a zoomed-out deck is shorter than the pane. */}\n        <div\n          className={`flex min-h-full flex-col ${\n            isTransitioning && isFitWidth ? \"overflow-clip\" : \"overflow-visible\"\n          }`}\n        >\n          <div\n            ref={documentSurfaceRef}\n            className={`relative mx-auto min-w-0 shrink-0 ${\n              // Block-axis centring only outside fit-width: at fit-width a\n              // pane resize re-fits the slides, and half of that height delta\n              // is motion the shell transform does not model. Zoomed, that\n              // transform is identity and the height is pane-independent.\n              isFitWidth ? \"\" : \"my-auto\"\n            }`}\n            data-slot=\"pptx-viewer-document-surface\"\n            style={{\n              contain: \"layout style\",\n              minWidth: layout.slideWidth,\n              width: layout.slideWidth,\n            }}\n          >\n            <div\n              ref={setCanvasRef}\n              className=\"relative h-full w-full\"\n              data-slot=\"pptx-slide-virtual-canvas\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction PptxSlideFrame({\n  source,\n  slideIndex,\n  layoutZoomScale,\n  rasterZoomScale,\n  rotation,\n  eager,\n  activity,\n  renderSlideOverlay,\n  getSlideRenderTiming,\n  isProjectedLive,\n  priority,\n  shouldRenderImmediately,\n}: {\n  source: PptxSource;\n  slideIndex: number;\n  // The scale the slide BOX is laid out at (the target of the current motion,\n  // re-fit every commit). Drives the DOM box the shell transform reprojects.\n  layoutZoomScale: number;\n  // The scale the CANVAS bitmap is rastered at. Held frozen through a\n  // transition so the box can re-fit without a re-raster; the bitmap is\n  // CSS-scaled into the box.\n  rasterZoomScale: number;\n  rotation: number;\n  eager: boolean;\n  activity: PptxScrollActivity;\n  renderSlideOverlay?: (props: PptxSlideOverlayProps) => React.ReactNode;\n  getSlideRenderTiming?: () =>\n    | ((timing: PptxSlideRenderTiming) => void)\n    | undefined;\n  isProjectedLive?: () => boolean;\n  priority: PptxSlideRenderPriority;\n  shouldRenderImmediately: boolean;\n}) {\n  const boxSize = getScaledSlideSize(source.baseSize, layoutZoomScale);\n  const visibleSize = getVisibleSlideSize(boxSize, rotation);\n\n  return (\n    <div\n      className=\"ring-border relative shadow-sm ring-1\"\n      style={{ width: visibleSize.width, height: visibleSize.height }}\n      data-slot=\"pptx-slide\"\n      data-slide-number={slideIndex + 1}\n    >\n      <div\n        className=\"absolute top-1/2 left-1/2\"\n        style={{\n          width: boxSize.width,\n          height: boxSize.height,\n          transform: `translate(-50%, -50%) rotate(${rotation}deg)`,\n        }}\n      >\n        <PptxSlideCanvas\n          source={source}\n          slideIndex={slideIndex}\n          layoutZoomScale={layoutZoomScale}\n          rasterZoomScale={rasterZoomScale}\n          eager={eager}\n          activity={activity}\n          getSlideRenderTiming={getSlideRenderTiming}\n          isProjectedLive={isProjectedLive}\n          priority={priority}\n          shouldRenderImmediately={shouldRenderImmediately}\n        />\n      </div>\n      {renderSlideOverlay ? (\n        <PptxSlideOverlay\n          slideNumber={slideIndex + 1}\n          visibleSize={visibleSize}\n          zoomScale={layoutZoomScale}\n          rotation={rotation}\n          renderSlideOverlay={renderSlideOverlay}\n        />\n      ) : null}\n    </div>\n  );\n}\n\nfunction PptxSlideCanvas({\n  source,\n  slideIndex,\n  layoutZoomScale,\n  rasterZoomScale,\n  eager,\n  activity,\n  getSlideRenderTiming,\n  isProjectedLive,\n  priority,\n  shouldRenderImmediately,\n}: {\n  source: PptxSource;\n  slideIndex: number;\n  layoutZoomScale: number;\n  rasterZoomScale: number;\n  eager: boolean;\n  activity: PptxScrollActivity;\n  getSlideRenderTiming?: () =>\n    | ((timing: PptxSlideRenderTiming) => void)\n    | undefined;\n  isProjectedLive?: () => boolean;\n  priority: PptxSlideRenderPriority;\n  shouldRenderImmediately: boolean;\n}) {\n  const rawDpr = typeof window !== \"undefined\" ? window.devicePixelRatio : 1;\n  const pixelRatio = getPptxRenderPixelRatio(rawDpr);\n  // Display size follows the box (target) scale; the backing store is rastered\n  // at the raster (possibly frozen) scale and CSS-scaled into it.\n  const slideSize = getScaledSlideSize(source.baseSize, layoutZoomScale);\n  const [renderState, setRenderState] =\n    React.useState<SlideRenderState>(\"idle\");\n  const canvasElementRef = React.useRef<HTMLCanvasElement | null>(null);\n\n  const canvasRef = React.useCallback((canvas: HTMLCanvasElement | null) => {\n    canvasElementRef.current = canvas;\n  }, []);\n\n  // Keyed on the RASTER scale (and stable priority primitives), never the\n  // layout scale — re-fitting the box to a new target must not re-raster the\n  // held bitmap. distanceFromReadingMarker is a scheduling hint only, so its\n  // per-frame drift is deliberately excluded from the raster key.\n  useKeyedLayoutEffect(\n    joinEffectKey([\n      activity,\n      eager,\n      getSlideRenderTiming,\n      isProjectedLive,\n      pixelRatio,\n      priority.isCurrentSlide,\n      priority.isInViewport,\n      priority.isScrollLead,\n      shouldRenderImmediately,\n      rasterZoomScale,\n      slideIndex,\n      source,\n    ]),\n    () => {\n      const canvas = canvasElementRef.current;\n      if (!canvas) return;\n\n      let cancelled = false;\n      const renderScale = rasterZoomScale * pixelRatio;\n      if (isProjectedLive && !isProjectedLive()) return;\n\n      const cachedResult = source.drawCachedBitmap({\n        canvas,\n        renderScale,\n        slideIndex,\n      });\n      if (cachedResult) {\n        notifySlideRenderTiming(getSlideRenderTiming?.(), {\n          cached: true,\n          durationMs: 0,\n          pixelRatio,\n          renderScale,\n          slideNumber: slideIndex + 1,\n          status: cachedResult.status,\n        });\n        if (cachedResult.status !== \"cancelled\") {\n          setRenderState(\n            cachedResult.status === \"failed\" ? \"failed\" : \"rendered\",\n          );\n        }\n        return () => {\n          cancelled = true;\n        };\n      }\n\n      setRenderState(\"rendering\");\n\n      const start = () => {\n        if (isProjectedLive && !isProjectedLive()) return;\n        const startedAt = now();\n        const startedCached = source.hasBitmap({ slideIndex, renderScale });\n        const cachedResult = source.drawCachedBitmap({\n          canvas,\n          renderScale,\n          slideIndex,\n        });\n        if (cachedResult) {\n          notifySlideRenderTiming(getSlideRenderTiming?.(), {\n            cached: true,\n            durationMs: now() - startedAt,\n            pixelRatio,\n            renderScale,\n            slideNumber: slideIndex + 1,\n            status: cachedResult.status,\n          });\n          if (cachedResult.status !== \"cancelled\") {\n            setRenderState(\n              cachedResult.status === \"failed\" ? \"failed\" : \"rendered\",\n            );\n          }\n          return;\n        }\n        source\n          .renderSlide({\n            slideIndex,\n            canvas,\n            renderScale,\n            isLive: () => !cancelled,\n            priority,\n          })\n          .then((result) => {\n            if (cancelled || (isProjectedLive && !isProjectedLive())) return;\n            notifySlideRenderTiming(getSlideRenderTiming?.(), {\n              cached: startedCached,\n              durationMs: now() - startedAt,\n              pixelRatio,\n              renderScale,\n              slideNumber: slideIndex + 1,\n              status: result.status,\n            });\n            if (result.status === \"cancelled\") return;\n            setRenderState(result.status === \"failed\" ? \"failed\" : \"rendered\");\n          })\n          .catch(() => {\n            if (cancelled || (isProjectedLive && !isProjectedLive())) return;\n            notifySlideRenderTiming(getSlideRenderTiming?.(), {\n              cached: startedCached,\n              durationMs: now() - startedAt,\n              pixelRatio,\n              renderScale,\n              slideNumber: slideIndex + 1,\n              status: \"failed\",\n            });\n            setRenderState(\"failed\");\n          });\n      };\n\n      if (shouldRenderImmediately || eager || !activity.isScrolling()) {\n        start();\n        return () => {\n          cancelled = true;\n        };\n      }\n\n      const off = activity.onIdle(() => {\n        if (!cancelled && (!isProjectedLive || isProjectedLive())) start();\n      });\n      return () => {\n        cancelled = true;\n        off();\n      };\n    },\n  );\n\n  return (\n    <>\n      <canvas\n        ref={canvasRef}\n        style={{ width: slideSize.width, height: slideSize.height }}\n        className=\"block h-full w-full bg-white\"\n      />\n      {renderState === \"failed\" ? (\n        <div className=\"bg-muted text-muted-foreground absolute inset-0 flex items-center justify-center p-4 text-center text-xs\">\n          Couldn&apos;t render slide {slideIndex + 1}.\n        </div>\n      ) : null}\n      {renderState !== \"rendered\" && renderState !== \"failed\" ? (\n        <Skeleton className=\"pointer-events-none absolute inset-0 rounded-none\" />\n      ) : null}\n    </>\n  );\n}\n\nfunction notifySlideRenderTiming(\n  callback: ((timing: PptxSlideRenderTiming) => void) | undefined,\n  timing: PptxSlideRenderTiming,\n) {\n  try {\n    callback?.(timing);\n  } catch {\n    /* Instrumentation callbacks must not affect slide rendering. */\n  }\n}\n\nfunction now() {\n  return typeof performance === \"undefined\" ? Date.now() : performance.now();\n}\n\ntype PptxSlideProjectionCache = {\n  canvas: HTMLDivElement | null;\n  hasMeasuredScroll: boolean;\n  lastScrollDirection: PptxScrollDirection;\n  lastScrollTop: number;\n  resetKey: string;\n  slides: Map<number, PptxProjectedSlide>;\n  window: PptxSlideProjectionWindow | null;\n};\n\ntype PptxSlideProjectionResult = {\n  fillOverscanNextFrame: boolean;\n};\n\ntype PptxSlideProjectionWindow = {\n  after: HTMLDivElement;\n  before: HTMLDivElement;\n  content: HTMLDivElement;\n  sticky: HTMLDivElement;\n};\n\ntype PptxProjectedSlide = {\n  activity: PptxScrollActivity | null;\n  getSlideRenderTiming: () =>\n    | ((timing: PptxSlideRenderTiming) => void)\n    | undefined;\n  isLive: boolean;\n  isProjectedLive: () => boolean;\n  onSlideRenderTiming:\n    | ((timing: PptxSlideRenderTiming) => void)\n    | null\n    | undefined;\n  // The zoom scale the slide's canvas bitmap was last rastered at. The slide's\n  // BOX tracks the target layout scale every commit (so the shell transform\n  // reprojects it continuously), but its raster is held at this scale through a\n  // transition — the frozen bitmap is CSS-scaled to the box, no re-raster.\n  rasterZoomScale: number | null;\n  renderSlideOverlay:\n    | ((props: PptxSlideOverlayProps) => React.ReactNode)\n    | null\n    | undefined;\n  renderKey: string;\n  root: Root;\n  shell: HTMLElement;\n  source: PptxSource | null;\n};\n\nconst pptxProjectionSourceKeys = new WeakMap<PptxSource, number>();\nlet nextPptxProjectionSourceKey = 1;\n\nfunction projectPptxSlides({\n  activity,\n  cache,\n  canvas,\n  eager,\n  getScrollMetrics,\n  isTransitioning,\n  layout,\n  onSlideRenderTiming,\n  renderSlideOverlay,\n  resetKey,\n  rotation,\n  source,\n  viewport,\n  zoomScale,\n}: {\n  activity: PptxScrollActivity;\n  cache: PptxSlideProjectionCache;\n  canvas: HTMLDivElement | null;\n  eager: boolean;\n  getScrollMetrics?: () => PptxScrollMetrics;\n  isTransitioning: boolean;\n  layout: PptxSlideLayout;\n  onSlideRenderTiming?: (timing: PptxSlideRenderTiming) => void;\n  renderSlideOverlay?: (props: PptxSlideOverlayProps) => React.ReactNode;\n  resetKey: string;\n  rotation: number;\n  source: PptxSource;\n  viewport: HTMLDivElement | null;\n  zoomScale: number;\n}): PptxSlideProjectionResult {\n  if (!canvas) return { fillOverscanNextFrame: false };\n\n  if (cache.canvas !== canvas) {\n    disposePptxSlideProjectionCache(cache);\n    cache.canvas = canvas;\n  }\n\n  const sourceKey = getPptxProjectionSourceKey(source);\n  const nextResetKey = `${sourceKey}:${resetKey}`;\n\n  // Slides mounted before the sidebar motion began. Held for the duration of the\n  // transition so the fit-width re-fit — which shifts the scroll model under the\n  // reader before the shell rebases it — can't churn the reading slide out of the\n  // mounted set. While transitioning, use this exact set: unioning with the new\n  // live window introduces add/remove churn in the sampled motion window.\n  const heldSlideIndexes =\n    isTransitioning && cache.slides.size > 0 ? [...cache.slides.keys()] : [];\n  const isHoldingTransitionWindow = heldSlideIndexes.length > 0;\n\n  if (cache.resetKey !== nextResetKey) {\n    // The fit-width re-fit changes zoomScale (part of the reset key), but the\n    // mounted slides stay valid — only their size/position changes, re-patched\n    // below. Disposing mid-transition would tear down and remount the reading\n    // slide, defeating the hold; keep the cache and adopt the new key in place.\n    if (heldSlideIndexes.length === 0) {\n      disposePptxSlideProjectionCache(cache);\n    }\n    cache.resetKey = nextResetKey;\n  }\n\n  const viewportHeight =\n    viewport?.clientHeight || viewport?.getBoundingClientRect().height || 0;\n  const metrics =\n    getScrollMetrics?.() ??\n    readPptxScrollMetrics({\n      scrollPageOffset: 0,\n      totalHeight: layout.totalHeight,\n      viewportElement: viewport,\n    });\n  const measuredScrollDirection = getPptxScrollDirection({\n    canMeasureDirection: cache.hasMeasuredScroll,\n    previousScrollTop: cache.lastScrollTop,\n    scrollTop: metrics.scrollTop,\n  });\n  const scrollDirection =\n    measuredScrollDirection === 0\n      ? cache.lastScrollDirection\n      : measuredScrollDirection;\n  if (measuredScrollDirection !== 0) {\n    cache.lastScrollDirection = measuredScrollDirection;\n  }\n  const fitPerfectly = shouldFitPptxPerfectly({\n    canFitPerfectly: cache.hasMeasuredScroll,\n    previousScrollTop: cache.lastScrollTop,\n    scrollTop: metrics.scrollTop,\n    viewportHeight: metrics.viewportHeight,\n  });\n  cache.hasMeasuredScroll = true;\n  cache.lastScrollTop = metrics.scrollTop;\n\n  setPptxStyle(canvas, \"contain\", \"layout style\");\n  setPptxPixelStyle(canvas, \"height\", metrics.physicalScrollHeight);\n  setPptxPixelStyle(canvas, \"min-width\", layout.slideWidth);\n  setPptxPixelStyle(canvas, \"width\", layout.slideWidth);\n\n  const liveSlides = getPptxRenderSlides({\n    fitPerfectly,\n    layout,\n    scrollTop: metrics.scrollTop,\n    viewportHeight: metrics.viewportHeight || viewportHeight,\n  });\n  const liveSlideIndexes = new Set(\n    liveSlides.map((virtualSlide) => virtualSlide.index),\n  );\n  // Held slides are kept mounted for continuity but must not re-raster: they\n  // retain their existing bitmap (rendered at the pre-motion scale) and are\n  // disposed once the delayed transition release runs.\n  const heldOnlyIndexes = new Set(\n    isHoldingTransitionWindow\n      ? heldSlideIndexes\n      : heldSlideIndexes.filter((index) => !liveSlideIndexes.has(index)),\n  );\n  const virtualSlides = isHoldingTransitionWindow\n    ? getHeldPptxVirtualSlides(heldSlideIndexes, layout)\n    : unionPptxVirtualSlides(liveSlides, heldSlideIndexes, layout);\n  const renderedWindow = getPptxRenderedSlideWindow({\n    layout,\n    physicalScrollHeight: metrics.physicalScrollHeight,\n    scrollPageOffset: metrics.scrollPageOffset,\n    slides: virtualSlides,\n    viewportHeight: metrics.viewportHeight || viewportHeight,\n  });\n  const visibleSlideIndexes = new Set(\n    virtualSlides.map((virtualSlide) => virtualSlide.index),\n  );\n  const projectionWindow = ensurePptxProjectionWindow(cache, canvas);\n\n  for (const [slideIndex, projectedSlide] of cache.slides) {\n    if (visibleSlideIndexes.has(slideIndex)) continue;\n    disposePptxProjectedSlide(projectedSlide);\n    cache.slides.delete(slideIndex);\n  }\n\n  if (!renderedWindow) {\n    syncPptxProjectionWindow(projectionWindow, {\n      afterHeight: metrics.physicalScrollHeight,\n      beforeHeight: 0,\n      height: 0,\n      stickyBottomInset: 0,\n      stickyTopInset: 0,\n    });\n    return { fillOverscanNextFrame: fitPerfectly };\n  }\n\n  syncPptxProjectionWindow(projectionWindow, renderedWindow);\n\n  let previousShell: HTMLElement | null = null;\n  for (const virtualSlide of renderedWindow.slides) {\n    const existingSlide = cache.slides.get(virtualSlide.index);\n    const projectedSlide =\n      existingSlide ?? createPptxProjectedSlide(virtualSlide);\n    patchPptxProjectedSlide(projectedSlide.shell, virtualSlide);\n    // A held-only slide that already has a bitmap keeps that bitmap (rastered\n    // at its frozen scale) — so the transition adds no raster work — but its\n    // BOX still re-fits to the target layout scale. The box drives the shell\n    // transform's continuity; freezing the box (as skipping the render did)\n    // makes the uniform counter-transform expose a scale jump at the retarget\n    // hand-off, where the reading-line content snaps. The frozen bitmap is\n    // CSS-scaled into the re-fit box, so the raster stays crisp-at-old-scale\n    // without a re-render blink.\n    const retainHeldRaster =\n      heldOnlyIndexes.has(virtualSlide.index) &&\n      existingSlide != null &&\n      existingSlide.rasterZoomScale != null &&\n      existingSlide.renderKey !== \"\";\n    const rasterZoomScale = retainHeldRaster\n      ? existingSlide.rasterZoomScale!\n      : zoomScale;\n    renderPptxProjectedSlide({\n      activity,\n      eager,\n      layoutZoomScale: zoomScale,\n      onSlideRenderTiming,\n      projectedSlide,\n      priority: getPptxSlideRenderPriority({\n        layout,\n        scrollDirection,\n        scrollTop: metrics.scrollTop,\n        viewportHeight: metrics.viewportHeight || viewportHeight,\n        virtualSlide,\n      }),\n      rasterZoomScale,\n      renderSlideOverlay,\n      rotation,\n      source,\n      virtualSlide,\n    });\n    cache.slides.set(virtualSlide.index, projectedSlide);\n    placePptxProjectedSlide(\n      projectionWindow.content,\n      projectedSlide.shell,\n      previousShell,\n    );\n    previousShell = projectedSlide.shell;\n  }\n\n  return { fillOverscanNextFrame: fitPerfectly };\n}\n\nfunction unionPptxVirtualSlides(\n  windowSlides: PptxVirtualSlide[],\n  heldIndexes: number[],\n  layout: PptxSlideLayout,\n): PptxVirtualSlide[] {\n  if (heldIndexes.length === 0) return windowSlides;\n\n  const byIndex = new Map(windowSlides.map((slide) => [slide.index, slide]));\n  for (const index of heldIndexes) {\n    if (byIndex.has(index)) continue;\n    if (index < 0 || index >= layout.slideCount) continue;\n    const slideNumber = index + 1;\n    byIndex.set(index, {\n      height: layout.slideHeight,\n      index,\n      key: String(slideNumber),\n      slideNumber,\n      top: getPptxSlideTop(layout, index),\n      width: layout.slideWidth,\n    });\n  }\n\n  return [...byIndex.values()].sort((a, b) => a.index - b.index);\n}\n\nfunction getHeldPptxVirtualSlides(\n  heldIndexes: number[],\n  layout: PptxSlideLayout,\n) {\n  return unionPptxVirtualSlides([], heldIndexes, layout);\n}\n\nfunction createPptxProjectedSlide(\n  virtualSlide: PptxVirtualSlide,\n): PptxProjectedSlide {\n  const shell = document.createElement(\"div\");\n  shell.className = \"absolute top-0 left-1/2\";\n  shell.dataset.slot = \"pptx-slide-slot\";\n  shell.dataset.virtualSlideNumber = String(virtualSlide.slideNumber);\n  // Stable identities so a box-only re-render (target scale commit) does not\n  // churn the canvas raster effect, whose key is object-identity sensitive.\n  const projectedSlide: PptxProjectedSlide = {\n    activity: null,\n    getSlideRenderTiming: () => projectedSlide.onSlideRenderTiming ?? undefined,\n    isLive: true,\n    isProjectedLive: () => projectedSlide.isLive,\n    onSlideRenderTiming: null,\n    rasterZoomScale: null,\n    renderSlideOverlay: null,\n    renderKey: \"\",\n    root: createRoot(shell),\n    shell,\n    source: null,\n  };\n  return projectedSlide;\n}\n\nfunction patchPptxProjectedSlide(\n  shell: HTMLElement,\n  virtualSlide: PptxVirtualSlide & { windowTop: number },\n) {\n  setPptxStyle(\n    shell,\n    \"transform\",\n    `translate(-50%, ${virtualSlide.windowTop}px)`,\n  );\n  setPptxPixelStyle(shell, \"width\", virtualSlide.width);\n  setPptxPixelStyle(shell, \"height\", virtualSlide.height);\n}\n\nfunction placePptxProjectedSlide(\n  canvas: HTMLElement,\n  shell: HTMLElement,\n  previousShell: HTMLElement | null,\n) {\n  const nextSibling = previousShell\n    ? previousShell.nextSibling\n    : canvas.firstChild;\n  if (shell === nextSibling) return;\n  canvas.insertBefore(shell, nextSibling);\n}\n\nfunction setPptxPixelStyle(\n  element: HTMLElement,\n  property: \"height\" | \"min-width\" | \"width\",\n  value: number,\n) {\n  setPptxStyle(element, property, `${value}px`);\n}\n\nfunction setPptxStyle(element: HTMLElement, property: string, value: string) {\n  if (element.style.getPropertyValue(property) === value) return;\n  element.style.setProperty(property, value);\n}\n\nfunction ensurePptxProjectionWindow(\n  cache: PptxSlideProjectionCache,\n  canvas: HTMLDivElement,\n): PptxSlideProjectionWindow {\n  const existing = cache.window;\n  if (existing?.before.parentElement === canvas) return existing;\n\n  const before = document.createElement(\"div\");\n  const sticky = document.createElement(\"div\");\n  const content = document.createElement(\"div\");\n  const after = document.createElement(\"div\");\n\n  before.dataset.slot = \"pptx-slide-window-before\";\n  sticky.dataset.slot = \"pptx-slide-sticky-window\";\n  content.dataset.slot = \"pptx-slide-sticky-content\";\n  after.dataset.slot = \"pptx-slide-window-after\";\n\n  before.style.contain = \"layout size\";\n  sticky.style.position = \"sticky\";\n  sticky.style.left = \"0\";\n  sticky.style.width = \"100%\";\n  sticky.style.overflow = \"visible\";\n  sticky.style.contain = \"layout style inline-size\";\n  sticky.style.isolation = \"isolate\";\n  sticky.style.display = \"flex\";\n  sticky.style.flexDirection = \"column\";\n  content.style.position = \"relative\";\n  content.style.width = \"100%\";\n  after.style.contain = \"layout size\";\n\n  sticky.append(content);\n  canvas.replaceChildren(before, sticky, after);\n\n  cache.window = { after, before, content, sticky };\n  return cache.window;\n}\n\nfunction syncPptxProjectionWindow(\n  projectionWindow: PptxSlideProjectionWindow,\n  renderedWindow: {\n    afterHeight: number;\n    beforeHeight: number;\n    height: number;\n    stickyBottomInset: number;\n    stickyTopInset: number;\n  },\n) {\n  setPptxPixelStyle(\n    projectionWindow.before,\n    \"height\",\n    renderedWindow.beforeHeight,\n  );\n  setPptxStyle(\n    projectionWindow.sticky,\n    \"top\",\n    `${renderedWindow.stickyTopInset}px`,\n  );\n  setPptxStyle(\n    projectionWindow.sticky,\n    \"bottom\",\n    `${renderedWindow.stickyBottomInset}px`,\n  );\n  setPptxPixelStyle(projectionWindow.sticky, \"height\", renderedWindow.height);\n  setPptxPixelStyle(projectionWindow.content, \"height\", renderedWindow.height);\n  setPptxPixelStyle(\n    projectionWindow.after,\n    \"height\",\n    renderedWindow.afterHeight,\n  );\n}\n\nfunction renderPptxProjectedSlide({\n  activity,\n  eager,\n  layoutZoomScale,\n  onSlideRenderTiming,\n  projectedSlide,\n  priority,\n  rasterZoomScale,\n  renderSlideOverlay,\n  rotation,\n  source,\n  virtualSlide,\n}: {\n  activity: PptxScrollActivity;\n  eager: boolean;\n  layoutZoomScale: number;\n  onSlideRenderTiming?: (timing: PptxSlideRenderTiming) => void;\n  projectedSlide: PptxProjectedSlide;\n  priority: PptxSlideRenderPriority;\n  rasterZoomScale: number;\n  renderSlideOverlay?: (props: PptxSlideOverlayProps) => React.ReactNode;\n  rotation: number;\n  source: PptxSource;\n  virtualSlide: PptxVirtualSlide;\n}) {\n  const renderKey = [\n    virtualSlide.index,\n    getPptxProjectionSourceKey(source),\n    source.baseSize.width,\n    source.baseSize.height,\n    layoutZoomScale,\n    rasterZoomScale,\n    rotation,\n    eager,\n    priority.isCurrentSlide,\n    priority.isInViewport,\n    priority.isScrollLead,\n  ].join(\"\\u0000\");\n  const shouldRender =\n    projectedSlide.renderKey !== renderKey ||\n    projectedSlide.source !== source ||\n    projectedSlide.activity !== activity ||\n    projectedSlide.renderSlideOverlay !== renderSlideOverlay;\n\n  projectedSlide.onSlideRenderTiming = onSlideRenderTiming;\n  projectedSlide.rasterZoomScale = rasterZoomScale;\n\n  if (!shouldRender) {\n    return;\n  }\n\n  projectedSlide.renderKey = renderKey;\n  projectedSlide.source = source;\n  projectedSlide.activity = activity;\n  projectedSlide.renderSlideOverlay = renderSlideOverlay;\n  projectedSlide.root.render(\n    <PptxSlideFrame\n      source={source}\n      slideIndex={virtualSlide.index}\n      layoutZoomScale={layoutZoomScale}\n      rasterZoomScale={rasterZoomScale}\n      rotation={rotation}\n      eager={eager}\n      activity={activity}\n      renderSlideOverlay={renderSlideOverlay}\n      getSlideRenderTiming={projectedSlide.getSlideRenderTiming}\n      isProjectedLive={projectedSlide.isProjectedLive}\n      priority={priority}\n      shouldRenderImmediately={\n        priority.isCurrentSlide ||\n        priority.isInViewport ||\n        priority.isScrollLead\n      }\n    />,\n  );\n}\n\nfunction getPptxSlideRenderPriority({\n  layout,\n  scrollDirection,\n  scrollTop,\n  viewportHeight,\n  virtualSlide,\n}: {\n  layout: PptxSlideLayout;\n  scrollDirection: PptxScrollDirection;\n  scrollTop: number;\n  viewportHeight: number;\n  virtualSlide: PptxVirtualSlide;\n}): PptxSlideRenderPriority {\n  const safeScrollTop =\n    Number.isFinite(scrollTop) && scrollTop > 0 ? scrollTop : 0;\n  const safeViewportHeight =\n    Number.isFinite(viewportHeight) && viewportHeight > 0\n      ? viewportHeight\n      : layout.slideHeight;\n  const marker = safeScrollTop + safeViewportHeight * PPTX_READING_MARKER_RATIO;\n  const slideTop = virtualSlide.top;\n  const slideBottom = slideTop + virtualSlide.height;\n  const viewportBottom = safeScrollTop + safeViewportHeight;\n  const isInViewport = slideBottom > safeScrollTop && slideTop < viewportBottom;\n\n  return {\n    distanceFromReadingMarker: Math.abs(\n      slideTop + virtualSlide.height / 2 - marker,\n    ),\n    isCurrentSlide: marker >= slideTop && marker < slideBottom,\n    isInViewport,\n    isScrollLead:\n      scrollDirection < 0 &&\n      !isInViewport &&\n      slideTop < safeScrollTop &&\n      slideBottom > safeScrollTop - PPTX_UPWARD_SCROLL_LEAD_PX,\n  };\n}\n\nfunction getPptxScrollDirection({\n  canMeasureDirection,\n  previousScrollTop,\n  scrollTop,\n}: {\n  canMeasureDirection: boolean;\n  previousScrollTop: number;\n  scrollTop: number;\n}): PptxScrollDirection {\n  if (!canMeasureDirection) return 0;\n  const delta = scrollTop - previousScrollTop;\n  if (delta < -1) return -1;\n  if (delta > 1) return 1;\n  return 0;\n}\n\nfunction disposePptxSlideProjectionCache(cache: PptxSlideProjectionCache) {\n  for (const projectedSlide of cache.slides.values()) {\n    disposePptxProjectedSlide(projectedSlide);\n  }\n  cache.slides.clear();\n  cache.hasMeasuredScroll = false;\n  cache.lastScrollDirection = 0;\n  cache.lastScrollTop = 0;\n  cache.window?.before.remove();\n  cache.window?.sticky.remove();\n  cache.window?.after.remove();\n  cache.window = null;\n}\n\nfunction shouldFitPptxPerfectly({\n  canFitPerfectly,\n  previousScrollTop,\n  scrollTop,\n  viewportHeight,\n}: {\n  canFitPerfectly: boolean;\n  previousScrollTop: number;\n  scrollTop: number;\n  viewportHeight: number;\n}) {\n  return (\n    canFitPerfectly &&\n    viewportHeight > 0 &&\n    Math.abs(scrollTop - previousScrollTop) >\n      viewportHeight + PPTX_RENDER_WINDOW_OVERSCAN_PX * 2\n  );\n}\n\nfunction disposePptxProjectedSlide(projectedSlide: PptxProjectedSlide) {\n  projectedSlide.isLive = false;\n  deferPptxRootUnmount(projectedSlide.root);\n  projectedSlide.shell.remove();\n}\n\nfunction deferPptxRootUnmount(root: Root) {\n  const unmount = () => root.unmount();\n  if (typeof queueMicrotask === \"function\") {\n    queueMicrotask(unmount);\n    return;\n  }\n  window.setTimeout(unmount, 0);\n}\n\nfunction assignPptxRef<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (!ref) return;\n  if (typeof ref === \"function\") {\n    ref(value);\n    return;\n  }\n  ref.current = value;\n}\n\nfunction getPptxProjectionSourceKey(source: PptxSource) {\n  const existingKey = pptxProjectionSourceKeys.get(source);\n  if (existingKey) return existingKey;\n  const key = nextPptxProjectionSourceKey;\n  nextPptxProjectionSourceKey += 1;\n  pptxProjectionSourceKeys.set(source, key);\n  return key;\n}\n\nfunction PptxSlideOverlay({\n  slideNumber,\n  visibleSize,\n  zoomScale,\n  rotation,\n  renderSlideOverlay,\n}: {\n  slideNumber: number;\n  visibleSize: PptxSize;\n  zoomScale: number;\n  rotation: number;\n  renderSlideOverlay: (props: PptxSlideOverlayProps) => React.ReactNode;\n}) {\n  return (\n    <div className=\"pointer-events-none absolute inset-0\">\n      {renderSlideOverlay({\n        slideNumber,\n        width: visibleSize.width,\n        height: visibleSize.height,\n        scale: zoomScale,\n        rotation,\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-slide.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/pptx-viewer-fallback.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\n\nimport { DEFAULT_PPTX_SLIDE_SIZE } from \"./pptx-viewer-core\";\nimport { ViewerControlsSkeleton } from \"./viewer-controls\";\n\nexport function PptxViewerFallback({\n  className,\n  bare = false,\n  fallbackSlideSize = DEFAULT_PPTX_SLIDE_SIZE,\n  controls = true,\n}: {\n  className?: string;\n  bare?: boolean;\n  fallbackSlideSize?: { width: number; height: number };\n  controls?: boolean;\n}) {\n  return (\n    <div\n      className={cn(\n        \"flex min-h-0 flex-col overflow-hidden\",\n        bare ? \"bg-muted/20 h-full\" : \"bg-muted/30 rounded-xl border\",\n        className,\n      )}\n      data-slot=\"pptx-viewer\"\n    >\n      {controls ? (\n        <ViewerControlsSkeleton position zoom rotate download />\n      ) : null}\n      <div className=\"min-h-0 flex-1 overflow-auto\">\n        <div className=\"flex flex-col items-center p-4\">\n          <PptxSlideSkeleton slideSize={fallbackSlideSize} />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction PptxSlideSkeleton({\n  slideSize,\n}: {\n  slideSize: { width: number; height: number };\n}) {\n  return (\n    <Skeleton\n      aria-hidden\n      className=\"ring-border w-full rounded-none shadow-sm ring-1\"\n      data-slot=\"pptx-slide-skeleton\"\n      style={{\n        aspectRatio: `${slideSize.width} / ${slideSize.height}`,\n      }}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/pptx-viewer-fallback.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/use-is-client.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nconst emptySubscribe = () => () => {};\nconst getClientSnapshot = () => true;\nconst getServerSnapshot = () => false;\n\n/**\n * SSR gate: `false` on the server (and during hydration's first pass),\n * `true` on the client.\n *\n * This must stay a synchronous external-store read, NOT the\n * `useState(false)` + mount-effect flip. The flip pattern makes every\n * viewer mount its Suspense boundary in a later update; when two such\n * boundaries suspend on pending resources in the same flush as other\n * commit-phase updates (viewer sidebar/geometry registration), React 19's\n * retry lanes desynchronize and re-attempt each other's boundary on every\n * commit — an unbounded synchronous suspend/retry loop that starves the\n * event loop (jsdom tests OOM; browsers busy-spin until the resource\n * resolves). With the store read, client renders suspend on mount, which\n * never enters that loop. Regression-guarded in\n * tests/pdf-viewer-thumbnails.test.tsx (\"shares one document resource…\").\n */\nexport function useIsClient(): boolean {\n  return React.useSyncExternalStore(\n    emptySubscribe,\n    getClientSnapshot,\n    getServerSnapshot,\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-is-client.ts"
    },
    {
      "path": "registry/new-york-v4/lib/viewer-errors.ts",
      "content": "export type ViewerFormat =\n  | \"pdf\"\n  | \"image\"\n  | \"text\"\n  | \"csv\"\n  | \"docx\"\n  | \"xlsx\"\n  | \"pptx\"\n  | \"file\";\n\nexport type ViewerErrorDomain =\n  | \"resource\"\n  | \"format\"\n  | \"state\"\n  | \"unsupported\"\n  | \"unknown\";\n\nexport type ResourceErrorKind =\n  | \"fetch_failed\"\n  | \"http_error\"\n  | \"aborted\"\n  | \"invalid_range\"\n  | \"partial_content\"\n  | \"too_large\"\n  | \"unsupported_capability\"\n  | \"unknown\";\n\nexport type ResourceTooLargeReason = \"bytes\" | \"lines\";\n\nexport class ResourceError extends Error {\n  readonly domain = \"resource\";\n  readonly kind: ResourceErrorKind;\n  readonly status?: number;\n  readonly tooLargeReason?: ResourceTooLargeReason;\n  override readonly cause?: unknown;\n\n  constructor({\n    kind,\n    message,\n    status,\n    tooLargeReason,\n    cause,\n  }: {\n    kind: ResourceErrorKind;\n    message: string;\n    status?: number;\n    tooLargeReason?: ResourceTooLargeReason;\n    cause?: unknown;\n  }) {\n    super(message);\n    this.name = \"ResourceError\";\n    this.kind = kind;\n    this.status = status;\n    this.tooLargeReason = tooLargeReason;\n    this.cause = cause;\n  }\n}\n\nexport type ViewerFormatErrorKind =\n  | \"bounds\"\n  | \"decode_failed\"\n  | \"disposed\"\n  | \"index_out_of_range\"\n  | \"load_failed\"\n  | \"parse_failed\"\n  | \"render_failed\"\n  | \"worker_failed\"\n  | \"unknown\";\n\nexport interface ViewerFormatErrorMapperOptions {\n  kind: ViewerFormatErrorKind;\n  message: string;\n}\n\nexport class ViewerFormatError extends Error {\n  readonly domain = \"format\";\n  readonly format: ViewerFormat;\n  readonly kind: ViewerFormatErrorKind;\n  override readonly cause?: unknown;\n\n  constructor({\n    format,\n    kind,\n    message,\n    cause,\n  }: {\n    format: ViewerFormat;\n    kind: ViewerFormatErrorKind;\n    message: string;\n    cause?: unknown;\n  }) {\n    super(message);\n    this.name = \"ViewerFormatError\";\n    this.format = format;\n    this.kind = kind;\n    this.cause = cause;\n  }\n}\n\nexport type ViewerStateErrorKind =\n  | \"invalid_bounds\"\n  | \"invalid_target\"\n  | \"out_of_range\"\n  | \"stale_resource\"\n  | \"unknown\";\n\nexport class ViewerStateError extends Error {\n  readonly domain = \"state\";\n  readonly format?: ViewerFormat;\n  readonly kind: ViewerStateErrorKind;\n  override readonly cause?: unknown;\n\n  constructor({\n    format,\n    kind,\n    message,\n    cause,\n  }: {\n    format?: ViewerFormat;\n    kind: ViewerStateErrorKind;\n    message: string;\n    cause?: unknown;\n  }) {\n    super(message);\n    this.name = \"ViewerStateError\";\n    this.format = format;\n    this.kind = kind;\n    this.cause = cause;\n  }\n}\n\nexport class ViewerUnsupportedError extends Error {\n  readonly domain = \"unsupported\";\n  readonly format?: ViewerFormat;\n  readonly sourceKind?: string;\n  override readonly cause?: unknown;\n\n  constructor({\n    format,\n    sourceKind,\n    message,\n    cause,\n  }: {\n    format?: ViewerFormat;\n    sourceKind?: string;\n    message: string;\n    cause?: unknown;\n  }) {\n    super(message);\n    this.name = \"ViewerUnsupportedError\";\n    this.format = format;\n    this.sourceKind = sourceKind;\n    this.cause = cause;\n  }\n}\n\nexport interface ViewerErrorInfo {\n  domain: ViewerErrorDomain;\n  format?: ViewerFormat;\n  kind: string;\n  message: string;\n  status?: number;\n  isRetryable: boolean;\n  isDownloadUseful: boolean;\n  userMessage: string;\n  cause?: unknown;\n}\n\nexport interface ViewerErrorContext {\n  format?: ViewerFormat;\n  sourceKind?: \"url\" | \"blob\" | \"text\";\n  canDownload?: boolean;\n  retry?: \"auto\" | \"always\" | \"never\";\n}\n\nexport function isAbortError(error: unknown): boolean {\n  return (\n    (error instanceof DOMException && error.name === \"AbortError\") ||\n    (error instanceof Error && error.name === \"AbortError\")\n  );\n}\n\nexport function isResourceError(error: unknown): error is ResourceError {\n  return (\n    error instanceof ResourceError ||\n    isErrorLike(error, \"ResourceError\", \"resource\")\n  );\n}\n\nexport function isViewerFormatError(\n  error: unknown,\n): error is ViewerFormatError {\n  return (\n    error instanceof ViewerFormatError ||\n    isErrorLike(error, \"ViewerFormatError\", \"format\")\n  );\n}\n\nexport function isViewerStateError(error: unknown): error is ViewerStateError {\n  return (\n    error instanceof ViewerStateError ||\n    isErrorLike(error, \"ViewerStateError\", \"state\")\n  );\n}\n\nexport function isViewerUnsupportedError(\n  error: unknown,\n): error is ViewerUnsupportedError {\n  return (\n    error instanceof ViewerUnsupportedError ||\n    isErrorLike(error, \"ViewerUnsupportedError\", \"unsupported\")\n  );\n}\n\nexport function toViewerErrorInfo(\n  error: unknown,\n  context: ViewerErrorContext = {},\n): ViewerErrorInfo {\n  const canDownload = context.canDownload ?? true;\n\n  if (isResourceError(error)) {\n    return {\n      domain: \"resource\",\n      format: context.format,\n      kind: error.kind,\n      message: error.message,\n      status: error.status,\n      isRetryable: retryable(\n        context,\n        resourceErrorDefaultRetry(error, context),\n      ),\n      isDownloadUseful: canDownload && error.kind !== \"aborted\",\n      userMessage: resourceErrorUserMessage(error),\n      cause: error.cause,\n    };\n  }\n\n  if (isViewerFormatError(error)) {\n    const format = error.format ?? context.format;\n    return {\n      domain: \"format\",\n      format,\n      kind: error.kind,\n      message: error.message,\n      isRetryable: retryable(\n        context,\n        formatErrorDefaultRetry(error, context, format),\n      ),\n      isDownloadUseful: canDownload,\n      userMessage: formatErrorUserMessage(format, error.kind, error),\n      cause: error.cause,\n    };\n  }\n\n  if (isViewerStateError(error)) {\n    return {\n      domain: \"state\",\n      format: error.format ?? context.format,\n      kind: error.kind,\n      message: error.message,\n      isRetryable: retryable(context, false),\n      isDownloadUseful: canDownload,\n      userMessage: stateErrorUserMessage(error.kind),\n      cause: error.cause,\n    };\n  }\n\n  if (isViewerUnsupportedError(error)) {\n    return {\n      domain: \"unsupported\",\n      format: error.format ?? context.format,\n      kind: \"unsupported\",\n      message: error.message,\n      isRetryable: retryable(context, false),\n      isDownloadUseful: canDownload,\n      userMessage: \"This file cannot be previewed here.\",\n      cause: error.cause,\n    };\n  }\n\n  if (isAbortError(error)) {\n    return {\n      domain: \"resource\",\n      format: context.format,\n      kind: \"aborted\",\n      message: \"Loading was cancelled.\",\n      isRetryable: retryable(context, false),\n      isDownloadUseful: false,\n      userMessage: \"Loading was cancelled.\",\n      cause: error,\n    };\n  }\n\n  const message = error instanceof Error ? error.message : String(error);\n  return {\n    domain: \"unknown\",\n    format: context.format,\n    kind: \"unknown\",\n    message,\n    isRetryable: retryable(context, unknownErrorDefaultRetry(context)),\n    isDownloadUseful: canDownload,\n    userMessage: fallbackUserMessage(context.format),\n    cause: error,\n  };\n}\n\nfunction isErrorLike(error: unknown, name: string, domain: ViewerErrorDomain) {\n  if (!error || typeof error !== \"object\") return false;\n  const candidate = error as {\n    name?: unknown;\n    domain?: unknown;\n    kind?: unknown;\n  };\n  return (\n    (candidate.name === name || candidate.domain === domain) &&\n    typeof candidate.kind === \"string\"\n  );\n}\n\nfunction retryable(context: ViewerErrorContext, fallback: boolean) {\n  if (context.retry === \"always\") return true;\n  if (context.retry === \"never\") return false;\n  return fallback;\n}\n\nfunction resourceErrorDefaultRetry(\n  error: ResourceError,\n  context: ViewerErrorContext,\n) {\n  if (error.kind === \"aborted\") return false;\n  if (error.kind === \"invalid_range\") return false;\n  if (error.kind === \"too_large\") return false;\n  if (error.kind === \"unsupported_capability\") return false;\n  return context.sourceKind === \"url\";\n}\n\nfunction formatErrorDefaultRetry(\n  error: ViewerFormatError,\n  context: ViewerErrorContext,\n  format: ViewerFormat | undefined,\n) {\n  if (format === \"text\" && error.kind === \"bounds\") return false;\n  if (error.kind === \"disposed\") return false;\n  if (error.kind === \"index_out_of_range\") return false;\n  if (format === \"docx\") return true;\n  return context.sourceKind === \"url\";\n}\n\nfunction unknownErrorDefaultRetry(context: ViewerErrorContext) {\n  if (context.format === \"docx\") return true;\n  return context.sourceKind === \"url\";\n}\n\nfunction resourceErrorUserMessage(error: ResourceError) {\n  if (error.kind === \"http_error\") {\n    return error.status\n      ? `Failed to load file: ${error.status}.`\n      : \"Couldn't load this file.\";\n  }\n  if (error.kind === \"fetch_failed\") return \"Couldn't load this file.\";\n  if (error.kind === \"aborted\") return \"Loading was cancelled.\";\n  if (error.kind === \"invalid_range\") return \"This source range is invalid.\";\n  if (error.kind === \"too_large\") {\n    return error.tooLargeReason === \"lines\"\n      ? \"This file has too many lines to preview.\"\n      : \"This file is too large to preview.\";\n  }\n  if (error.kind === \"partial_content\") {\n    return \"This source returned partial content and cannot be previewed here.\";\n  }\n  if (error.kind === \"unsupported_capability\") {\n    return \"This source cannot be previewed here.\";\n  }\n  return \"Couldn't load this file.\";\n}\n\nfunction formatErrorUserMessage(\n  format: ViewerFormat | undefined,\n  kind: string,\n  error?: unknown,\n) {\n  if (format === \"pdf\") return \"Couldn't load this PDF.\";\n  if (format === \"image\") {\n    if (kind === \"index_out_of_range\")\n      return \"This image page is out of range.\";\n    if (kind === \"decode_failed\") return \"Couldn't decode this image.\";\n    return \"Couldn't load this image.\";\n  }\n  if (format === \"text\") {\n    if (kind === \"render_failed\") return \"Couldn't render this text file.\";\n    if (kind === \"bounds\") {\n      const boundsError = error as {\n        reason?: unknown;\n        boundName?: unknown;\n      };\n      if (boundsError.reason === \"lines\") {\n        return \"This text file has too many lines to preview.\";\n      }\n      if (boundsError.reason === \"bytes\") {\n        return \"This text file is too large to preview.\";\n      }\n      if (typeof boundsError.boundName === \"string\") {\n        return \"Text viewer bounds are invalid.\";\n      }\n    }\n    return \"Couldn't load this text file.\";\n  }\n  if (format === \"csv\") return \"Couldn't parse this table.\";\n  if (format === \"docx\") return \"Couldn't render this document.\";\n  if (format === \"xlsx\") return \"Couldn't parse this spreadsheet.\";\n  if (format === \"pptx\") {\n    if (kind === \"render_failed\") return \"Couldn't render this slide.\";\n    return \"Couldn't load this presentation.\";\n  }\n  return \"Couldn't load this file.\";\n}\n\nfunction stateErrorUserMessage(kind: ViewerStateErrorKind) {\n  if (kind === \"invalid_bounds\") return \"Viewer bounds are invalid.\";\n  if (kind === \"invalid_target\") return \"The requested target is invalid.\";\n  if (kind === \"out_of_range\") return \"The requested item is out of range.\";\n  if (kind === \"stale_resource\") return \"This viewer state is no longer valid.\";\n  return \"Couldn't load this file.\";\n}\n\nfunction fallbackUserMessage(format: ViewerFormat | undefined) {\n  if (format === \"pdf\") return \"Couldn't load this PDF.\";\n  if (format === \"image\") return \"Couldn't load this image.\";\n  if (format === \"text\") return \"Couldn't load this text file.\";\n  if (format === \"csv\") return \"Couldn't parse this table.\";\n  if (format === \"docx\") return \"Couldn't load this document.\";\n  if (format === \"xlsx\") return \"Couldn't load this spreadsheet.\";\n  if (format === \"pptx\") return \"Couldn't load this presentation.\";\n  return \"Couldn't load this file.\";\n}\n",
      "type": "registry:lib",
      "target": "@lib/viewer-errors.ts"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-context.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type { FileCategory } from \"@/lib/viewer-source\";\nimport type { FileViewerElementRegistry } from \"./file-viewer-elements\";\nimport type { FileViewerMotionKernel } from \"./file-viewer-motion-kernel\";\n\nexport type FileViewerSidebarMode = \"inline\" | \"overlay\";\nexport type FileViewerSidebarRequestedMode = \"auto\" | FileViewerSidebarMode;\nexport type FileViewerSidebarState = \"expanded\" | \"collapsed\";\nexport type FileViewerSidebarSide = \"left\" | \"right\";\nexport type FileViewerSidebarCollapsible = \"offcanvas\" | \"none\";\n\nexport type FileViewerHeaderMode = \"inline\" | \"outlets\";\n\nexport const DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH = \"10rem\";\n\nexport type FileViewerSetSidebarOpen = (\n  value: boolean | ((isSidebarOpen: boolean) => boolean),\n) => void;\n\nexport type FileViewerSidebarOpenProps = {\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n};\n\nexport type FileViewerContextValue = {\n  headerMode: FileViewerHeaderMode;\n  hasHeaderOutlets: boolean;\n  isInsideFileViewer: boolean;\n  resourceCategory: FileCategory;\n  sidebarOpenProps: FileViewerSidebarOpenProps;\n};\n\nexport type FileViewerSidebarRegistration = {\n  collapsible: FileViewerSidebarCollapsible;\n  id: string;\n  side: FileViewerSidebarSide;\n  width: string;\n  widthPixels: number;\n};\n\nexport type FileViewerSidebarValue = {\n  canToggleSidebar: boolean;\n  isSidebarInteractive: boolean;\n  isSidebarOpen: boolean;\n  mode: FileViewerSidebarMode;\n  side: FileViewerSidebarSide;\n  sidebarId: string;\n  sidebarState: FileViewerSidebarState;\n  setSidebarOpen: FileViewerSetSidebarOpen;\n  toggleSidebar: () => void;\n};\n\nexport type FileViewerShellStaticContextValue = {\n  canToggleSidebar: boolean;\n  collapsible: FileViewerSidebarCollapsible;\n  elementRegistry: FileViewerElementRegistry;\n  mode: FileViewerSidebarMode;\n  motionDurationMs: number;\n  motionKernel: FileViewerMotionKernel;\n  registerSidebar: (registration: FileViewerSidebarRegistration) => () => void;\n  rootId: string;\n  setSidebarOpen: FileViewerSetSidebarOpen;\n  side: FileViewerSidebarSide;\n  sidebarId: string;\n  sidebarWidth: string;\n  toggleSidebar: () => void;\n};\n\nexport type FileViewerSidebarDynamicContextValue = {\n  isSidebarInteractive: boolean;\n  isSidebarOpen: boolean;\n  isSidebarTransitioning: boolean;\n  sidebarState: FileViewerSidebarState;\n};\n\nexport const FileViewerContext = React.createContext<FileViewerContextValue>({\n  headerMode: \"inline\",\n  hasHeaderOutlets: false,\n  isInsideFileViewer: false,\n  resourceCategory: \"unsupported\",\n  sidebarOpenProps: {},\n});\n\nexport const FileViewerShellStaticContext =\n  React.createContext<FileViewerShellStaticContextValue | null>(null);\n\nexport const FileViewerSidebarDynamicContext =\n  React.createContext<FileViewerSidebarDynamicContextValue | null>(null);\n\nexport function useFileViewerContext() {\n  return React.useContext(FileViewerContext);\n}\n\nexport function useOptionalFileViewerShellStatic() {\n  return React.useContext(FileViewerShellStaticContext);\n}\n\nexport function useFileViewerShellStatic(consumer: string) {\n  const context = React.useContext(FileViewerShellStaticContext);\n  if (!context) {\n    throw new Error(`${consumer} must be rendered inside FileViewer.`);\n  }\n  return context;\n}\n\nexport function useOptionalFileViewerShell() {\n  const staticContext = React.useContext(FileViewerShellStaticContext);\n  const sidebarContext = React.useContext(FileViewerSidebarDynamicContext);\n\n  return React.useMemo(\n    () =>\n      staticContext && sidebarContext\n        ? { ...staticContext, ...sidebarContext }\n        : null,\n    [sidebarContext, staticContext],\n  );\n}\n\nexport function useFileViewerShell(consumer: string) {\n  const context = useOptionalFileViewerShell();\n  if (!context) {\n    throw new Error(`${consumer} must be rendered inside FileViewer.`);\n  }\n  return context;\n}\n\nexport function useFileViewerSidebar(): FileViewerSidebarValue {\n  const fileViewerContext = React.useContext(FileViewerContext);\n  const shellContext = useOptionalFileViewerShell();\n\n  if (!fileViewerContext.isInsideFileViewer || !shellContext) {\n    throw new Error(\"useFileViewerSidebar must be used within FileViewer.\");\n  }\n\n  return shellContext;\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-context.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-elements.ts",
      "content": "\"use client\";\n\nimport type {\n  FileViewerDocumentSurface,\n  FileViewerMotionKernel,\n} from \"./file-viewer-motion-kernel\";\nimport type { FileViewerMotionFrame } from \"./file-viewer-motion-plan\";\n\nexport const FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT =\n  \"file-viewer:before-layout-motion\";\n\n// The kernel dispatches the before-layout-motion event as a CustomEvent whose\n// detail is its LIVE interactive frame, so renderers can capture pre-commit\n// anchors against what is actually on screen (a mid-flight retarget sees the\n// settled layout plus the in-flight transform).\nexport function readFileViewerBeforeLayoutMotionFrame(\n  event: Event,\n): FileViewerMotionFrame | null {\n  if (!(event instanceof CustomEvent)) return null;\n  const detail: unknown = event.detail;\n  return detail != null && typeof detail === \"object\"\n    ? (detail as FileViewerMotionFrame)\n    : null;\n}\n\nexport type FileViewerElements = {\n  documentSurfaceElement: HTMLElement | null;\n  getDocumentSurfaceMotionProbeElement: (() => HTMLElement | null) | null;\n  sidebarElement: HTMLElement | null;\n  sidebarGapElement: HTMLDivElement | null;\n  sidebarTriggerElement: HTMLElement | null;\n  viewerShellElement: HTMLDivElement | null;\n};\n\nexport type FileViewerElementRegistry = {\n  getElements: () => FileViewerElements;\n  registerDocumentSurface: (surface: FileViewerDocumentSurface) => () => void;\n  registerSidebarElement: (element: HTMLElement | null) => void;\n  registerSidebarGapElement: (element: HTMLDivElement | null) => void;\n  registerSidebarTriggerElement: (element: HTMLElement | null) => void;\n  registerViewerShellElement: (element: HTMLDivElement | null) => void;\n};\n\nexport function createFileViewerElementRegistry({\n  motionKernel,\n  onViewerShellElementChange,\n}: {\n  motionKernel: FileViewerMotionKernel;\n  onViewerShellElementChange: (element: HTMLDivElement | null) => void;\n}): FileViewerElementRegistry {\n  const elements: FileViewerElements = {\n    documentSurfaceElement: null,\n    getDocumentSurfaceMotionProbeElement: null,\n    sidebarElement: null,\n    sidebarGapElement: null,\n    sidebarTriggerElement: null,\n    viewerShellElement: null,\n  };\n  let documentSurfaceRegistration = 0;\n\n  return {\n    getElements: () => elements,\n    registerDocumentSurface: (surface) => {\n      documentSurfaceRegistration += 1;\n      const registration = documentSurfaceRegistration;\n      elements.documentSurfaceElement = surface.element;\n      elements.getDocumentSurfaceMotionProbeElement =\n        surface.getMotionProbeElement ?? null;\n      motionKernel.setDocumentSurface(surface);\n      return () => {\n        if (documentSurfaceRegistration !== registration) return;\n        elements.documentSurfaceElement = null;\n        elements.getDocumentSurfaceMotionProbeElement = null;\n        motionKernel.setDocumentSurface(null);\n      };\n    },\n    registerSidebarElement: (element) => {\n      if (elements.sidebarElement === element) return;\n      elements.sidebarElement = element;\n    },\n    registerSidebarGapElement: (element) => {\n      if (elements.sidebarGapElement === element) return;\n      elements.sidebarGapElement = element;\n      motionKernel.setSidebarGapElement(element);\n    },\n    registerSidebarTriggerElement: (element) => {\n      elements.sidebarTriggerElement = element;\n    },\n    registerViewerShellElement: (element) => {\n      if (elements.viewerShellElement === element) return;\n      elements.viewerShellElement = element;\n      onViewerShellElementChange(element);\n    },\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-elements.ts"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-motion-kernel.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { flushSync } from \"react-dom\";\n\nimport { FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT } from \"./file-viewer-elements\";\nimport {\n  easeFileViewerMotion,\n  FILE_VIEWER_MOTION_DURATION_MS,\n  areFileViewerMotionFramesEqual,\n  areFileViewerMotionRestFramesEqual,\n  createFileViewerIdleMotionFrame,\n  createFileViewerMotionPlan,\n  createFileViewerMotionRestFrame,\n  type FileViewerMotionFrame,\n  type FileViewerMotionPlan,\n  type FileViewerMotionRestFrame,\n  type FileViewerMotionTarget,\n} from \"./file-viewer-motion-plan\";\n\n// The kernel is the single owner of sidebar motion. It holds the clock (one\n// rAF loop from slide start through settle), writes the continuous inline\n// styles, and publishes to React subscribers only at phase edges\n// (idle → sliding → settling → idle). Everything discrete — data attributes,\n// inert/aria, the overlay translate classes — is owned by React renders.\n//\n// Commit-then-relax ordering: the first sliding frame is flushed\n// synchronously inside the toggle's own task, so renderers commit their\n// TARGET layout and rebase scroll before anything paints; the per-tick style\n// writes then only relax the counter-transform to identity. Settle removes a\n// no-op transform — it never commits layout, so no flushSync runs inside rAF.\nexport type FileViewerMotionKernel = {\n  getFlightRecords: () => readonly FileViewerMotionFlightRecord[];\n  getInteractiveSnapshot: () => FileViewerMotionFrame;\n  getSnapshot: () => FileViewerMotionFrame;\n  setDocumentSurface: (surface: FileViewerDocumentSurface | null) => void;\n  setSidebarGapElement: (element: HTMLElement | null) => void;\n  startMotion: (target: FileViewerMotionTarget) => void;\n  subscribe: (listener: () => void) => () => void;\n  syncTarget: (target: FileViewerMotionTarget) => void;\n};\n\n// Always-on flight recorder: every motion leaves a bounded trace (per-tick\n// widths, phase edges, settle holds, inter-frame gaps) so a blink report is\n// diagnosable after the fact without re-instrumenting.\nexport type FileViewerMotionFlightRecord = {\n  fromInlineSize: number;\n  id: number;\n  interrupted: boolean;\n  maxTickGapMs: number;\n  open: boolean;\n  settleHoldFrameCount: number;\n  startedAt: number;\n  ticks: FileViewerMotionFlightTick[];\n  toInlineSize: number;\n};\n\nexport type FileViewerMotionFlightTick = {\n  elapsedMs: number;\n  phase: FileViewerMotionFrame[\"phase\"];\n  sidebarInlineSize: number;\n};\n\nconst FILE_VIEWER_FLIGHT_RECORD_LIMIT = 8;\nconst FILE_VIEWER_FLIGHT_TICK_LIMIT = 240;\n\nexport type FileViewerDocumentSurface = {\n  element: HTMLElement;\n  getMotionProbeElement?: (() => HTMLElement | null) | null;\n  readSettleSnapshot?: FileViewerDocumentSurfaceSettleSnapshotReader | null;\n  resolveMotionStyle?: FileViewerDocumentSurfaceMotionResolver | null;\n};\n\nexport type FileViewerDocumentSurfaceSettleSnapshotReader = () =>\n  | readonly number[]\n  | null\n  | undefined;\n\n// Layout reads are quarantined outside the kernel (viewer-measurement / the\n// frame controller layer). The kernel owns time and style writes only, so the\n// settle-hold rect reader is injected by its creator rather than imported.\nexport type FileViewerElementRectSnapshotReader = (\n  element: HTMLElement | null,\n) => readonly number[];\n\nexport type FileViewerMotionKernelOptions = {\n  readElementRectSnapshot?: FileViewerElementRectSnapshotReader | null;\n};\n\nexport type FileViewerDocumentSurfaceMotionStyle = {\n  customProperties?: Readonly<Record<string, string | null>>;\n  transform: string;\n  transformOrigin: string;\n  willChange: string;\n};\n\nexport type FileViewerDocumentSurfaceMotionResolver = (\n  frame: FileViewerMotionFrame,\n) => FileViewerDocumentSurfaceMotionStyle | null;\n\ntype FileViewerActiveMotion = {\n  durationMs: number;\n  from: FileViewerMotionRestFrame;\n  // The clock re-anchors to the first tick's vsync frame time: startedAt is\n  // stamped inside the toggle's task, but the synchronous slide-start commit\n  // can burn 10ms+ before anything paints, and an ease anchored at the click\n  // lands its first painted frame that deep into the curve.\n  hasFrameClockAnchor: boolean;\n  id: number;\n  startedAt: number;\n  to: FileViewerMotionRestFrame;\n};\n\ntype FileViewerSettleRelease = {\n  idleFrame: FileViewerMotionFrame;\n  lastSnapshot: readonly number[];\n  remainingFrameCount: number;\n  settlingFrame: FileViewerMotionFrame;\n  stableFrameCount: number;\n};\n\nconst FILE_VIEWER_SETTLE_SCROLL_EPSILON_PX = 0.25;\nconst FILE_VIEWER_SETTLE_STABLE_FRAME_COUNT = 2;\nconst FILE_VIEWER_SETTLE_MAX_HOLD_FRAMES = 6;\nconst FILE_VIEWER_SUBPIXEL_ENDPOINT_EPSILON_PX = 1;\n\nexport const DEFAULT_FILE_VIEWER_MOTION_FRAME: FileViewerMotionFrame = {\n  shellInlineSize: 0,\n  durationMs: FILE_VIEWER_MOTION_DURATION_MS,\n  fromInlineSize: 0,\n  layoutInlineSize: 0,\n  mode: \"overlay\",\n  motionId: null,\n  motionProgress: 1,\n  open: false,\n  phase: \"idle\",\n  side: \"left\",\n  sidebarInlineSize: 0,\n  sidebarWidth: 0,\n  toInlineSize: 0,\n};\n\nexport function createFileViewerMotionKernel({\n  readElementRectSnapshot = null,\n}: FileViewerMotionKernelOptions = {}): FileViewerMotionKernel {\n  const listeners = new Set<() => void>();\n  let contractFrame = DEFAULT_FILE_VIEWER_MOTION_FRAME;\n  let interactiveFrame = DEFAULT_FILE_VIEWER_MOTION_FRAME;\n  let target: FileViewerMotionTarget = {\n    shellInlineSize: 0,\n    durationMs: DEFAULT_FILE_VIEWER_MOTION_FRAME.durationMs,\n    mode: DEFAULT_FILE_VIEWER_MOTION_FRAME.mode,\n    open: DEFAULT_FILE_VIEWER_MOTION_FRAME.open,\n    side: DEFAULT_FILE_VIEWER_MOTION_FRAME.side,\n    sidebarWidth: 0,\n  };\n  let documentSurface: FileViewerDocumentSurface | null = null;\n  let documentSurfaceCustomProperties = new Set<string>();\n  let sidebarGapElement: HTMLElement | null = null;\n  let activeMotion: FileViewerActiveMotion | null = null;\n  let settleRelease: FileViewerSettleRelease | null = null;\n  let rafHandle = 0;\n  let settleReleaseHandle = 0;\n  let motionSequence = 0;\n  const flightRecords: FileViewerMotionFlightRecord[] = [];\n  let activeFlightRecord: FileViewerMotionFlightRecord | null = null;\n  let lastFlightTickAt = 0;\n\n  const beginFlightRecord = (motion: FileViewerActiveMotion) => {\n    if (activeFlightRecord && activeFlightRecord.id !== motion.id) {\n      activeFlightRecord.interrupted = true;\n    }\n    activeFlightRecord = {\n      fromInlineSize: motion.from.layoutInlineSize,\n      id: motion.id,\n      interrupted: false,\n      maxTickGapMs: 0,\n      open: motion.to.open,\n      settleHoldFrameCount: 0,\n      startedAt: motion.startedAt,\n      ticks: [],\n      toInlineSize: motion.to.layoutInlineSize,\n    };\n    lastFlightTickAt = motion.startedAt;\n    flightRecords.push(activeFlightRecord);\n    if (flightRecords.length > FILE_VIEWER_FLIGHT_RECORD_LIMIT) {\n      flightRecords.splice(\n        0,\n        flightRecords.length - FILE_VIEWER_FLIGHT_RECORD_LIMIT,\n      );\n    }\n  };\n\n  const recordFlightTick = (frame: FileViewerMotionFrame, now = readNow()) => {\n    const record = activeFlightRecord;\n    if (!record || frame.motionId !== record.id) return;\n    record.maxTickGapMs = Math.max(record.maxTickGapMs, now - lastFlightTickAt);\n    lastFlightTickAt = now;\n    if (frame.phase === \"settling\") record.settleHoldFrameCount += 1;\n    if (record.ticks.length >= FILE_VIEWER_FLIGHT_TICK_LIMIT) return;\n    record.ticks.push({\n      elapsedMs: Math.max(0, now - record.startedAt),\n      phase: frame.phase,\n      sidebarInlineSize: frame.sidebarInlineSize,\n    });\n  };\n\n  const notify = () => {\n    for (const listener of listeners) listener();\n  };\n\n  const publishContractFrame = (\n    nextFrame: FileViewerMotionFrame,\n    { flushSubscribers = false }: { flushSubscribers?: boolean } = {},\n  ) => {\n    if (areFileViewerMotionFramesEqual(contractFrame, nextFrame)) return;\n    contractFrame = nextFrame;\n\n    if (flushSubscribers) {\n      flushSync(notify);\n      return;\n    }\n\n    notify();\n  };\n\n  // The gap's inline size and the document surface's counter-scale must land\n  // in the same frame: two independent CSS transitions (width on the gap,\n  // transform on the surface) can desync under main-thread jank, letting the\n  // document edge drift off the sidebar edge mid-slide. The kernel therefore\n  // writes both here, once per tick.\n  const writeElementStyles = (nextFrame: FileViewerMotionFrame) => {\n    writeSidebarGapStyle(nextFrame);\n    writeDocumentSurfaceStyle(nextFrame);\n  };\n\n  const commit = (\n    nextFrame: FileViewerMotionFrame,\n    { publish = true }: { publish?: boolean } = {},\n  ) => {\n    writeElementStyles(nextFrame);\n    interactiveFrame = nextFrame;\n    if (publish) publishContractFrame(nextFrame);\n  };\n\n  const cancelTick = () => {\n    if (rafHandle === 0) return;\n    getCancelAnimationFrame()(rafHandle);\n    rafHandle = 0;\n  };\n\n  const cancelSettleRelease = () => {\n    settleRelease = null;\n    if (settleReleaseHandle === 0) return;\n    getCancelAnimationFrame()(settleReleaseHandle);\n    settleReleaseHandle = 0;\n  };\n\n  const readMotionSample = (\n    motion: FileViewerActiveMotion,\n    now = readNow(),\n  ): FileViewerMotionFrame => {\n    const rawTimeProgress =\n      motion.durationMs <= 0\n        ? 1\n        : clamp((now - motion.startedAt) / motion.durationMs, 0, 1);\n    const rawMotionProgress = easeFileViewerMotion(rawTimeProgress);\n    const rawSidebarInlineSize = lerp(\n      motion.from.sidebarInlineSize,\n      motion.to.sidebarInlineSize,\n      rawMotionProgress,\n    );\n    const isSubpixelEndpoint =\n      rawMotionProgress > 0.98 &&\n      Math.abs(rawSidebarInlineSize - motion.to.sidebarInlineSize) <=\n        FILE_VIEWER_SUBPIXEL_ENDPOINT_EPSILON_PX;\n    const motionProgress = isSubpixelEndpoint ? 1 : rawMotionProgress;\n    const sidebarInlineSize = isSubpixelEndpoint\n      ? motion.to.sidebarInlineSize\n      : rawSidebarInlineSize;\n    const layoutInlineSize = Math.max(\n      0,\n      motion.to.shellInlineSize - sidebarInlineSize,\n    );\n    const fromInlineSize = motion.from.layoutInlineSize;\n\n    return {\n      shellInlineSize: motion.to.shellInlineSize,\n      durationMs: motion.durationMs,\n      fromInlineSize,\n      layoutInlineSize,\n      mode: motion.to.mode,\n      motionId: motion.id,\n      motionProgress,\n      open: motion.to.open,\n      phase: motionProgress < 1 ? \"sliding\" : \"settling\",\n      side: motion.to.side,\n      sidebarInlineSize,\n      sidebarWidth: motion.to.sidebarWidth,\n      toInlineSize: motion.to.layoutInlineSize,\n    };\n  };\n\n  const settle = () => {\n    if (!activeMotion) return;\n    const finishedMotion = activeMotion;\n    activeMotion = null;\n    cancelTick();\n\n    const idleFrame = createFileViewerIdleMotionFrame(finishedMotion.to);\n    const settlingFrame: FileViewerMotionFrame = {\n      ...idleFrame,\n      fromInlineSize: finishedMotion.from.layoutInlineSize,\n      motionId: finishedMotion.id,\n      phase: \"settling\",\n    };\n\n    // Layout and scroll were committed at slide start; settling only clears\n    // the (now identity) counter-transform and holds until shell geometry\n    // stops moving. Nothing here re-renders geometry, so no flushSync in rAF.\n    commit(settlingFrame, { publish: false });\n    recordFlightTick(settlingFrame);\n    publishContractFrame(settlingFrame);\n    scheduleSettleRelease(settlingFrame, idleFrame);\n  };\n\n  // Ticks sample the clock at the rAF FRAME timestamp, never the callback's\n  // execution time: the frame time is the vsync the paint belongs to, and a\n  // callback running late in a janky frame would otherwise write a position\n  // ahead of the frame's own time axis — a real paint-side velocity excess\n  // (the probes' rule 11, applied to the writer). The first tick also\n  // re-anchors startedAt to its frame time, so the ease starts at the first\n  // paintable frame rather than at the click that precedes the slide-start\n  // commit.\n  const tick = (frameTime: number) => {\n    rafHandle = 0;\n    if (!activeMotion) return;\n    const now = Number.isFinite(frameTime) ? frameTime : readNow();\n    if (!activeMotion.hasFrameClockAnchor) {\n      activeMotion.hasFrameClockAnchor = true;\n      activeMotion.startedAt = now;\n    }\n    const sample = readMotionSample(activeMotion, now);\n    if (sample.motionProgress >= 1) {\n      settle();\n      return;\n    }\n    commit(sample, { publish: false });\n    recordFlightTick(sample, now);\n    scheduleTick();\n  };\n\n  const scheduleTick = () => {\n    if (rafHandle !== 0) return;\n    rafHandle = getRequestAnimationFrame()(tick);\n  };\n\n  const scheduleSettleRelease = (\n    settlingFrame: FileViewerMotionFrame,\n    idleFrame: FileViewerMotionFrame,\n  ) => {\n    cancelSettleRelease();\n    settleRelease = {\n      idleFrame,\n      lastSnapshot: readSettleSnapshot(),\n      remainingFrameCount: FILE_VIEWER_SETTLE_MAX_HOLD_FRAMES,\n      settlingFrame,\n      stableFrameCount: 0,\n    };\n    scheduleSettleReleaseFrame();\n  };\n\n  const scheduleSettleReleaseFrame = () => {\n    if (settleReleaseHandle !== 0) return;\n    settleReleaseHandle = getRequestAnimationFrame()(holdSettleRelease);\n  };\n\n  const holdSettleRelease = () => {\n    settleReleaseHandle = 0;\n    if (!settleRelease) return;\n\n    commit(settleRelease.settlingFrame, { publish: false });\n    recordFlightTick(settleRelease.settlingFrame);\n\n    const snapshot = readSettleSnapshot();\n    const stableFrameCount = areSettleSnapshotsEqual(\n      settleRelease.lastSnapshot,\n      snapshot,\n    )\n      ? settleRelease.stableFrameCount + 1\n      : 0;\n    const remainingFrameCount = settleRelease.remainingFrameCount - 1;\n\n    if (\n      stableFrameCount >= FILE_VIEWER_SETTLE_STABLE_FRAME_COUNT ||\n      remainingFrameCount <= 0\n    ) {\n      const idleFrame = settleRelease.idleFrame;\n      settleRelease = null;\n      // Natural completion: close the flight record so the next motion does\n      // not mark this one interrupted.\n      activeFlightRecord = null;\n      commit(idleFrame);\n      return;\n    }\n\n    settleRelease = {\n      ...settleRelease,\n      lastSnapshot: snapshot,\n      remainingFrameCount,\n      stableFrameCount,\n    };\n    scheduleSettleReleaseFrame();\n  };\n\n  // The event carries the kernel's LIVE frame so renderers can capture their\n  // pre-commit anchor against what is actually on screen — during a mid-flight\n  // retarget that is the settled layout PLUS the in-flight transform, not the\n  // settled layout alone.\n  const dispatchBeforeLayoutMotion = (currentFrame: FileViewerMotionFrame) => {\n    documentSurface?.element.dispatchEvent(\n      new CustomEvent<FileViewerMotionFrame>(\n        FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,\n        { detail: currentFrame },\n      ),\n    );\n  };\n\n  const interruptActiveFlightRecord = () => {\n    if (!activeFlightRecord) return;\n    activeFlightRecord.interrupted = true;\n    activeFlightRecord = null;\n  };\n\n  const retarget = (nextTarget: FileViewerMotionTarget, animate: boolean) => {\n    cancelSettleRelease();\n    // Continuity is with what is PAINTED, not with the clock: mid-flight the\n    // screen shows the last tick's commit (`interactiveFrame`), which can be\n    // a frame behind a fresh clock sample. Planning (and the before-motion\n    // capture renderers do off the event detail) from the painted frame keeps\n    // the retarget hand-off pixel-continuous; the new motion simply re-lerps\n    // from the painted geometry.\n    const currentFrame =\n      interactiveFrame.shellInlineSize > 0\n        ? interactiveFrame\n        : createFileViewerIdleMotionFrame(\n            createFileViewerMotionRestFrame(target),\n          );\n    const plan = createFileViewerMotionPlan({\n      animate: animate && !prefersReducedMotion(),\n      currentFrame,\n      nextTarget,\n    });\n    if (shouldDispatchBeforeLayoutMotion(plan)) {\n      dispatchBeforeLayoutMotion(currentFrame);\n    }\n    target = plan.resolvedTarget;\n\n    if (!plan.shouldAnimate) {\n      if (activeMotion) interruptActiveFlightRecord();\n      activeMotion = null;\n      cancelTick();\n      commit(createFileViewerIdleMotionFrame(plan.nextRestFrame));\n      return;\n    }\n\n    motionSequence += 1;\n    activeMotion = {\n      durationMs: plan.resolvedTarget.durationMs,\n      from: { ...plan.currentRestFrame, layoutInlineSize: plan.fromInlineSize },\n      hasFrameClockAnchor: false,\n      id: motionSequence,\n      startedAt: readNow(),\n      to: plan.nextRestFrame,\n    };\n    beginFlightRecord(activeMotion);\n    const startFrame = readMotionSample(activeMotion, activeMotion.startedAt);\n    // Commit the discontinuity while it cannot be seen: flush the first\n    // sliding frame synchronously (inside the toggle's own task) so renderers\n    // lay out at the target width and rebase scroll before first paint, hidden\n    // behind the counter-transform written above in the same task.\n    writeElementStyles(startFrame);\n    interactiveFrame = startFrame;\n    recordFlightTick(startFrame, activeMotion.startedAt);\n    publishContractFrame(startFrame, { flushSubscribers: true });\n    scheduleTick();\n  };\n\n  const syncTarget = (nextTarget: FileViewerMotionTarget) => {\n    const nextRestFrame = createFileViewerMotionRestFrame(nextTarget);\n\n    if (activeMotion) {\n      target = nextTarget;\n      if (areFileViewerMotionRestFramesEqual(activeMotion.to, nextRestFrame)) {\n        return;\n      }\n      // A mode flip mid-motion (breakpoint crossing during the slide) cannot\n      // be animated: React re-renders the new mode immediately, so an inline\n      // slide continuing against overlay DOM (or vice versa) double-moves the\n      // surface. Snap to the new rest geometry instead.\n      if (nextRestFrame.mode !== activeMotion.to.mode) {\n        interruptActiveFlightRecord();\n        activeMotion = null;\n        cancelTick();\n        cancelSettleRelease();\n        commit(createFileViewerIdleMotionFrame(nextRestFrame));\n        return;\n      }\n      retarget(nextTarget, true);\n      return;\n    }\n\n    cancelSettleRelease();\n    target = nextTarget;\n    commit(createFileViewerIdleMotionFrame(nextRestFrame));\n  };\n\n  return {\n    getFlightRecords: () => flightRecords.slice(),\n    getInteractiveSnapshot: () =>\n      activeMotion ? readMotionSample(activeMotion) : interactiveFrame,\n    getSnapshot: () => contractFrame,\n    setDocumentSurface: (surface) => {\n      const previousSurface = documentSurface;\n      if (\n        previousSurface &&\n        (!surface || previousSurface.element !== surface.element)\n      ) {\n        clearDocumentSurfaceStyle(previousSurface.element);\n      }\n      documentSurface = surface;\n      writeDocumentSurfaceStyle(interactiveFrame);\n    },\n    setSidebarGapElement: (element) => {\n      sidebarGapElement = element;\n      writeSidebarGapStyle(interactiveFrame);\n    },\n    startMotion: (nextTarget) => retarget(nextTarget, true),\n    subscribe: (listener) => {\n      listeners.add(listener);\n      return () => {\n        listeners.delete(listener);\n      };\n    },\n    syncTarget,\n  };\n\n  function writeSidebarGapStyle(nextFrame: FileViewerMotionFrame) {\n    if (!sidebarGapElement) return;\n\n    // Overlay motion is CSS-owned; relinquish the gap so its `w-0` class is\n    // the only writer outside inline mode.\n    if (nextFrame.mode !== \"inline\") {\n      sidebarGapElement.style.width = \"\";\n      sidebarGapElement.style.flexBasis = \"\";\n      return;\n    }\n\n    sidebarGapElement.style.width = `${nextFrame.sidebarInlineSize}px`;\n    sidebarGapElement.style.flexBasis = `${nextFrame.sidebarInlineSize}px`;\n  }\n\n  function writeDocumentSurfaceStyle(nextFrame: FileViewerMotionFrame) {\n    if (!documentSurface) return;\n\n    const { element, resolveMotionStyle } = documentSurface;\n    const resolvedStyle = resolveMotionStyle?.(nextFrame);\n    if (resolvedStyle) {\n      writeDocumentSurfaceCustomProperties(\n        element,\n        resolvedStyle.customProperties,\n      );\n      element.style.transform = resolvedStyle.transform;\n      element.style.transformOrigin = resolvedStyle.transformOrigin;\n      element.style.willChange = resolvedStyle.willChange;\n      return;\n    }\n\n    // Default (no motion resolver): identity. Fit-width renderers register\n    // the shared commit-then-relax resolver (file-viewer-fit-width-motion);\n    // a surface without one either tracks the live DOM width on its own or\n    // opts out of shell motion entirely, and must not be transformed here.\n    writeDocumentSurfaceCustomProperties(element, null);\n    element.style.transform = \"\";\n    element.style.transformOrigin = \"\";\n    element.style.willChange = \"\";\n  }\n\n  function writeDocumentSurfaceCustomProperties(\n    element: HTMLElement,\n    customProperties:\n      | Readonly<Record<string, string | null>>\n      | null\n      | undefined,\n  ) {\n    const nextNames = new Set(Object.keys(customProperties ?? {}));\n    for (const name of documentSurfaceCustomProperties) {\n      if (!nextNames.has(name)) {\n        element.style.removeProperty(name);\n      }\n    }\n\n    for (const [name, value] of Object.entries(customProperties ?? {})) {\n      if (value == null) {\n        element.style.removeProperty(name);\n      } else {\n        element.style.setProperty(name, value);\n      }\n    }\n\n    documentSurfaceCustomProperties = nextNames;\n  }\n\n  function clearDocumentSurfaceStyle(element: HTMLElement) {\n    element.style.transform = \"\";\n    element.style.transformOrigin = \"\";\n    element.style.willChange = \"\";\n    for (const name of documentSurfaceCustomProperties) {\n      element.style.removeProperty(name);\n    }\n    documentSurfaceCustomProperties = new Set();\n  }\n\n  function readSettleSnapshot(): readonly number[] {\n    const values: number[] = [];\n\n    appendElementRectSnapshot(values, sidebarGapElement);\n    appendElementRectSnapshot(values, documentSurface?.element ?? null);\n\n    try {\n      const surfaceSnapshot = documentSurface?.readSettleSnapshot?.();\n      if (surfaceSnapshot) {\n        values.push(...surfaceSnapshot.map(toSettleSnapshotNumber));\n      }\n    } catch {\n      // A renderer snapshot is diagnostic, not correctness-critical. If a\n      // renderer unmounts while settling, fall back to shell geometry.\n    }\n\n    return values.length > 0 ? values : [0];\n  }\n\n  function appendElementRectSnapshot(\n    values: number[],\n    element: HTMLElement | null,\n  ) {\n    if (!readElementRectSnapshot || !element) return;\n    for (const value of readElementRectSnapshot(element)) {\n      values.push(toSettleSnapshotNumber(value));\n    }\n  }\n}\n\nfunction areSettleSnapshotsEqual(\n  previous: readonly number[],\n  next: readonly number[],\n) {\n  if (previous.length !== next.length) return false;\n  return previous.every(\n    (value, index) =>\n      Math.abs(value - next[index]) <= FILE_VIEWER_SETTLE_SCROLL_EPSILON_PX,\n  );\n}\n\nfunction toSettleSnapshotNumber(value: number) {\n  return Number.isFinite(value) ? value : 0;\n}\n\nfunction shouldDispatchBeforeLayoutMotion({\n  currentRestFrame,\n  nextRestFrame,\n}: FileViewerMotionPlan) {\n  return (\n    currentRestFrame.mode === \"inline\" &&\n    nextRestFrame.mode === \"inline\" &&\n    Math.abs(\n      currentRestFrame.layoutInlineSize - nextRestFrame.layoutInlineSize,\n    ) > 0.001\n  );\n}\n\nexport function useFileViewerMotionFrame(\n  kernel: FileViewerMotionKernel | null | undefined,\n): FileViewerMotionFrame {\n  const subscribe = React.useCallback(\n    (listener: () => void) => kernel?.subscribe(listener) ?? (() => {}),\n    [kernel],\n  );\n  const getSnapshot = React.useCallback(\n    () => kernel?.getSnapshot() ?? DEFAULT_FILE_VIEWER_MOTION_FRAME,\n    [kernel],\n  );\n\n  return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\nfunction prefersReducedMotion() {\n  return (\n    typeof matchMedia === \"function\" &&\n    matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n\nfunction readNow() {\n  return typeof performance !== \"undefined\" &&\n    typeof performance.now === \"function\"\n    ? performance.now()\n    : Date.now();\n}\n\nfunction getRequestAnimationFrame() {\n  return (\n    globalThis.requestAnimationFrame ??\n    ((callback: FrameRequestCallback) =>\n      window.setTimeout(() => callback(readNow()), 16))\n  );\n}\n\nfunction getCancelAnimationFrame() {\n  return (\n    globalThis.cancelAnimationFrame ??\n    ((id: number) => {\n      window.clearTimeout(id);\n    })\n  );\n}\n\nfunction lerp(from: number, to: number, progress: number) {\n  return from + (to - from) * progress;\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/file-viewer-motion-kernel.ts"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-renderer-contract.ts",
      "content": "\"use client\";\n\nimport {\n  getFileViewerMotionRasterInlineSize,\n  type FileViewerMotionFrame,\n  type FileViewerMotionPhase,\n} from \"./file-viewer-motion-plan\";\nimport type { ViewerDocumentTransition } from \"./viewer-types\";\n\nexport type FileViewerDocumentAlign = \"start\" | \"center\" | \"end\";\n\n// Physical inline direction of the document frame (computed CSS `direction`).\n// The fit-width motion transform works on the physical X axis, so it needs\n// the direction to model where auto-margin alignment actually puts the stage.\nexport type FileViewerInlineDirection = \"ltr\" | \"rtl\";\n\n// `phase` is the motion clock's state; `documentTransition` is the single\n// spelling of the policies derived from it. Renderers read layout/scroll/\n// visual decisions from the transition, never from duplicated top-level\n// fields. `isTransitioning` is shorthand for `phase === \"sliding\"`.\nexport type FileViewerRendererFrame = {\n  align: FileViewerDocumentAlign;\n  canToggleSidebar: boolean;\n  direction: FileViewerInlineDirection;\n  documentTransition: ViewerDocumentTransition;\n  element: HTMLDivElement | null;\n  fromInlineSize: number | null;\n  isTransitioning: boolean;\n  layoutInlineSize: number | null;\n  motionDurationMs: number;\n  phase: FileViewerMotionPhase;\n  rasterInlineSize: number | null;\n  settledInlineSize: number | null;\n  shellInlineSize: number | null;\n  toInlineSize: number | null;\n  usesShellGeometry: boolean;\n};\n\nexport function resolveFileViewerRendererLayoutInlineSize({\n  fallbackInlineSize,\n  rendererFrame,\n}: {\n  fallbackInlineSize: number | null;\n  rendererFrame: FileViewerRendererFrame;\n}) {\n  const fallbackSize = resolveMeasuredInlineSize(fallbackInlineSize);\n\n  // Commit-then-relax: the renderer lays out at the motion's TARGET width for\n  // the entire motion (layoutPolicy \"target\" from the first sliding frame).\n  // The in-flight visual is the surface motion transform reprojecting that\n  // settled layout, so settle never commits layout.\n  if (\n    rendererFrame.documentTransition.layoutPolicy === \"target\" &&\n    rendererFrame.toInlineSize != null\n  ) {\n    return rendererFrame.toInlineSize;\n  }\n\n  return rendererFrame.layoutInlineSize ?? fallbackSize;\n}\n\nexport function createFileViewerRendererFrame({\n  align,\n  canToggleSidebar,\n  direction = \"ltr\",\n  element,\n  fallbackInlineSize,\n  motionFrame,\n  motionDurationMs,\n  usesShellGeometry,\n}: {\n  align: FileViewerDocumentAlign;\n  canToggleSidebar: boolean;\n  direction?: FileViewerInlineDirection;\n  element: HTMLDivElement | null;\n  fallbackInlineSize: number | null;\n  motionFrame: FileViewerMotionFrame;\n  motionDurationMs: number;\n  usesShellGeometry: boolean;\n}): FileViewerRendererFrame {\n  const measuredInlineSize = resolveMeasuredInlineSize(fallbackInlineSize);\n  const shellInlineSize = usesShellGeometry\n    ? motionFrame.shellInlineSize\n    : null;\n  const layoutInlineSize = usesShellGeometry\n    ? motionFrame.layoutInlineSize\n    : measuredInlineSize;\n  const settledInlineSize = usesShellGeometry\n    ? motionFrame.toInlineSize\n    : measuredInlineSize;\n  const rasterInlineSize = usesShellGeometry\n    ? getFileViewerMotionRasterInlineSize(motionFrame)\n    : layoutInlineSize;\n  const fromInlineSize = usesShellGeometry\n    ? motionFrame.fromInlineSize\n    : settledInlineSize;\n  const toInlineSize = usesShellGeometry\n    ? motionFrame.toInlineSize\n    : settledInlineSize;\n  const documentTransition = createFileViewerRendererTransition({\n    motionFrame,\n    usesShellGeometry,\n  });\n\n  const phase = usesShellGeometry ? motionFrame.phase : \"idle\";\n\n  return {\n    align,\n    canToggleSidebar,\n    direction,\n    documentTransition,\n    element,\n    fromInlineSize,\n    isTransitioning: phase === \"sliding\",\n    layoutInlineSize,\n    motionDurationMs,\n    phase,\n    rasterInlineSize,\n    settledInlineSize: settledInlineSize ?? layoutInlineSize,\n    shellInlineSize,\n    toInlineSize: toInlineSize ?? layoutInlineSize,\n    usesShellGeometry,\n  };\n}\n\nfunction resolveMeasuredInlineSize(inlineSize: number | null | undefined) {\n  return inlineSize != null && Number.isFinite(inlineSize) && inlineSize > 0\n    ? inlineSize\n    : null;\n}\n\nfunction createFileViewerRendererTransition({\n  motionFrame,\n  usesShellGeometry,\n}: {\n  motionFrame: FileViewerMotionFrame;\n  usesShellGeometry: boolean;\n}): ViewerDocumentTransition {\n  if (!usesShellGeometry) {\n    return {\n      layoutPolicy: \"live\",\n      scrollPolicy: \"preserve\",\n      source: \"none\",\n      transitionId: null,\n      visualPolicy: \"none\",\n    };\n  }\n\n  switch (motionFrame.phase) {\n    // Sliding commits the TARGET layout immediately (inside the toggle's own\n    // task, before first paint) and rebases scroll to the reading anchor in\n    // the same commit; the shell transform hides the jump. Settling then has\n    // no layout or scroll work left — it only clears the identity transform.\n    case \"sliding\":\n      return {\n        layoutPolicy: \"target\",\n        scrollPolicy: \"rebase\",\n        source: \"viewer-shell\",\n        transitionId: motionFrame.motionId,\n        visualPolicy: \"shell-transform\",\n      };\n    case \"settling\":\n      return {\n        layoutPolicy: \"target\",\n        scrollPolicy: \"rebase\",\n        source: \"viewer-shell\",\n        transitionId: motionFrame.motionId,\n        visualPolicy: \"shell-transform\",\n      };\n    case \"idle\":\n      return {\n        layoutPolicy: \"live\",\n        scrollPolicy: \"preserve\",\n        source: \"none\",\n        transitionId: null,\n        visualPolicy: \"none\",\n      };\n  }\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-renderer-contract.ts"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-renderer-frame.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  useOptionalFileViewerShell,\n  useOptionalFileViewerShellStatic,\n  useFileViewerShellStatic,\n} from \"./file-viewer-context\";\nimport type { FileViewerDocumentSurface } from \"./file-viewer-motion-kernel\";\nimport {\n  createFileViewerRendererFrame,\n  type FileViewerDocumentAlign,\n  type FileViewerRendererFrame,\n} from \"./file-viewer-renderer-contract\";\nimport { useFileViewerMotionFrame } from \"./file-viewer-motion-kernel\";\nimport { useViewerInlineDirection } from \"./viewer-measurement\";\n\nexport type FileViewerRendererEnvironment = {\n  registerDocumentSurface: (surface: FileViewerDocumentSurface) => () => void;\n  usesShellGeometry: boolean;\n};\n\nexport type FileViewerDocumentFrameState = {\n  align: FileViewerDocumentAlign;\n  element: HTMLDivElement | null;\n  inlineSize: number | null;\n};\n\nconst FileViewerDocumentFrameContext =\n  React.createContext<FileViewerDocumentFrameState | null>(null);\n\nexport function FileViewerDocumentFrameProvider({\n  children,\n  value,\n}: {\n  children: React.ReactNode;\n  value: FileViewerDocumentFrameState;\n}) {\n  return (\n    <FileViewerDocumentFrameContext.Provider value={value}>\n      {children}\n    </FileViewerDocumentFrameContext.Provider>\n  );\n}\n\nexport function useOptionalFileViewerDocumentFrame(): FileViewerDocumentFrameState | null {\n  return React.useContext(FileViewerDocumentFrameContext);\n}\n\nexport function useOptionalFileViewerRendererEnvironment(): FileViewerRendererEnvironment {\n  const { elementRegistry, usesShellGeometry } =\n    useFileViewerRendererEnvironmentState();\n  const registerDocumentSurface = React.useCallback(\n    (surface: FileViewerDocumentSurface) =>\n      elementRegistry?.registerDocumentSurface(surface) ?? (() => {}),\n    [elementRegistry],\n  );\n\n  return React.useMemo(\n    () => ({\n      registerDocumentSurface,\n      usesShellGeometry,\n    }),\n    [registerDocumentSurface, usesShellGeometry],\n  );\n}\n\nexport type FileViewerSidebarMotion = {\n  /** True when the shell animates the sidebar (inline mode with a toggle). */\n  isMotionManaged: boolean;\n  isSidebarInteractive: boolean;\n  isSidebarOpen: boolean;\n  isSidebarTransitioning: boolean;\n};\n\nexport function useOptionalFileViewerSidebarMotion(): FileViewerSidebarMotion | null {\n  const shell = useOptionalFileViewerShell();\n\n  return React.useMemo(\n    () =>\n      shell\n        ? {\n            isMotionManaged: shell.mode === \"inline\" && shell.canToggleSidebar,\n            isSidebarInteractive: shell.isSidebarInteractive,\n            isSidebarOpen: shell.isSidebarOpen,\n            isSidebarTransitioning: shell.isSidebarTransitioning,\n          }\n        : null,\n    [shell],\n  );\n}\n\nexport function useFileViewerRendererFrame({\n  fallbackInlineSize,\n}: {\n  fallbackInlineSize?: number | null;\n} = {}): FileViewerRendererFrame {\n  useFileViewerShellStatic(\"useFileViewerRendererFrame\");\n  return useResolvedFileViewerRendererFrame({\n    fallbackInlineSize,\n    required: true,\n  });\n}\n\nexport function useOptionalFileViewerRendererFrame({\n  fallbackInlineSize,\n}: {\n  fallbackInlineSize?: number | null;\n} = {}): FileViewerRendererFrame {\n  return useResolvedFileViewerRendererFrame({\n    fallbackInlineSize,\n    required: false,\n  });\n}\n\nfunction useResolvedFileViewerRendererFrame({\n  fallbackInlineSize,\n  required,\n}: {\n  fallbackInlineSize?: number | null;\n  required: boolean;\n}): FileViewerRendererFrame {\n  const { motionFrame, shell, usesShellGeometry } =\n    useFileViewerRendererEnvironmentState();\n  const documentFrame = useOptionalFileViewerDocumentFrame();\n\n  if (required && !documentFrame) {\n    throw new Error(\n      \"useFileViewerRendererFrame must be used within FileViewerInset.\",\n    );\n  }\n\n  const fallbackSize =\n    fallbackInlineSize != null && Number.isFinite(fallbackInlineSize)\n      ? fallbackInlineSize\n      : null;\n\n  // The fit-width motion transform is a physical-X computation, so renderers\n  // need the frame's computed CSS `direction` alongside its logical align.\n  const direction = useViewerInlineDirection(documentFrame?.element ?? null);\n\n  return React.useMemo(\n    () =>\n      createFileViewerRendererFrame({\n        align: documentFrame?.align ?? \"center\",\n        canToggleSidebar: shell?.canToggleSidebar ?? false,\n        direction,\n        element: documentFrame?.element ?? null,\n        fallbackInlineSize: documentFrame?.inlineSize ?? fallbackSize,\n        motionFrame,\n        motionDurationMs: shell?.motionDurationMs ?? 0,\n        usesShellGeometry,\n      }),\n    [\n      direction,\n      documentFrame?.align,\n      documentFrame?.element,\n      documentFrame?.inlineSize,\n      fallbackSize,\n      shell?.canToggleSidebar,\n      motionFrame,\n      shell?.motionDurationMs,\n      usesShellGeometry,\n    ],\n  );\n}\n\nfunction useFileViewerRendererEnvironmentState() {\n  const shell = useOptionalFileViewerShellStatic();\n  const motionFrame = useFileViewerMotionFrame(shell?.motionKernel);\n  const usesShellGeometry = Boolean(\n    shell &&\n      motionFrame.shellInlineSize > 0 &&\n      shell.mode === \"inline\" &&\n      (shell.canToggleSidebar || shell.collapsible === \"none\"),\n  );\n\n  return React.useMemo(\n    () => ({\n      elementRegistry: shell?.elementRegistry,\n      motionFrame,\n      shell,\n      usesShellGeometry,\n    }),\n    [motionFrame, shell, usesShellGeometry],\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-renderer-frame.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-motion-plan.ts",
      "content": "\"use client\";\n\nimport type {\n  FileViewerSidebarMode,\n  FileViewerSidebarSide,\n} from \"./file-viewer-context\";\n\nexport const FILE_VIEWER_MOTION_EPSILON = 0.5;\n\n// The one duration for every sidebar motion timeline — the kernel clock, the\n// motion targets, and the overlay panel's CSS transition all read this value.\nexport const FILE_VIEWER_MOTION_DURATION_MS = 150;\n\n// The one easing for every sidebar motion timeline. Cubic ease-out: the slide\n// decelerates into rest. Linear progress ends at full velocity, and content\n// far from the reading anchor (the bottom of a tall fit-width document)\n// travels several pixels per millisecond straight into a hard stop — a\n// visible jolt the anchor line never shows.\nexport function easeFileViewerMotion(timeProgress: number) {\n  return 1 - (1 - timeProgress) ** 3;\n}\n\nexport type FileViewerMotionPhase = \"idle\" | \"sliding\" | \"settling\";\n\nexport type FileViewerMotionTarget = {\n  shellInlineSize: number;\n  durationMs: number;\n  mode: FileViewerSidebarMode;\n  open: boolean;\n  side: FileViewerSidebarSide;\n  sidebarWidth: number;\n};\n\nexport type FileViewerMotionFrame = {\n  shellInlineSize: number;\n  durationMs: number;\n  fromInlineSize: number;\n  layoutInlineSize: number;\n  mode: FileViewerSidebarMode;\n  motionId: number | null;\n  motionProgress: number;\n  open: boolean;\n  phase: FileViewerMotionPhase;\n  side: FileViewerSidebarSide;\n  sidebarInlineSize: number;\n  sidebarWidth: number;\n  toInlineSize: number;\n};\n\nexport type FileViewerMotionRestFrame = Pick<\n  FileViewerMotionFrame,\n  | \"shellInlineSize\"\n  | \"durationMs\"\n  | \"layoutInlineSize\"\n  | \"mode\"\n  | \"open\"\n  | \"side\"\n  | \"sidebarInlineSize\"\n  | \"sidebarWidth\"\n>;\n\nexport type FileViewerMotionPlan = {\n  currentRestFrame: FileViewerMotionRestFrame;\n  fromInlineSize: number;\n  nextRestFrame: FileViewerMotionRestFrame;\n  resolvedTarget: FileViewerMotionTarget;\n  shouldAnimate: boolean;\n};\n\nexport function createFileViewerMotionRestFrame(\n  target: FileViewerMotionTarget,\n): FileViewerMotionRestFrame {\n  const shellInlineSize = target.shellInlineSize;\n  const sidebarInlineSize =\n    target.mode === \"inline\" && target.open\n      ? Math.min(target.sidebarWidth, shellInlineSize)\n      : 0;\n\n  return {\n    shellInlineSize,\n    durationMs: target.durationMs,\n    layoutInlineSize: Math.max(0, shellInlineSize - sidebarInlineSize),\n    mode: target.mode,\n    open: target.open,\n    side: target.side,\n    sidebarInlineSize,\n    sidebarWidth: target.sidebarWidth,\n  };\n}\n\nexport function createFileViewerIdleMotionFrame(\n  restFrame: FileViewerMotionRestFrame,\n): FileViewerMotionFrame {\n  return {\n    ...restFrame,\n    fromInlineSize: restFrame.layoutInlineSize,\n    motionId: null,\n    motionProgress: 1,\n    phase: \"idle\",\n    toInlineSize: restFrame.layoutInlineSize,\n  };\n}\n\nexport function getFileViewerMotionRasterInlineSize(\n  frame: Pick<\n    FileViewerMotionFrame,\n    \"fromInlineSize\" | \"layoutInlineSize\" | \"toInlineSize\"\n  >,\n): number {\n  return Math.max(\n    frame.fromInlineSize,\n    frame.toInlineSize,\n    frame.layoutInlineSize,\n  );\n}\n\nexport function createFileViewerMotionPlan({\n  animate,\n  currentFrame,\n  nextTarget,\n}: {\n  animate: boolean;\n  currentFrame: FileViewerMotionFrame;\n  nextTarget: FileViewerMotionTarget;\n}): FileViewerMotionPlan {\n  const resolvedTarget = resolveFileViewerMotionTarget({\n    currentFrame,\n    nextTarget,\n  });\n  const currentRestFrame = pickFileViewerMotionRestFrame(currentFrame);\n  const nextRestFrame = createFileViewerMotionRestFrame(resolvedTarget);\n  // The motion's visual origin is what is on screen RIGHT NOW: for a fresh\n  // motion that is the rest layout; for a mid-flight retarget it is the live\n  // interpolated width, so the new motion's first frame (and every renderer's\n  // anchor solve) continues from the picture the reader is looking at rather\n  // than the interrupted motion's origin.\n  const fromInlineSize =\n    currentFrame.phase === \"sliding\"\n      ? currentFrame.layoutInlineSize\n      : currentRestFrame.layoutInlineSize;\n  const shouldAnimate =\n    animate &&\n    resolvedTarget.mode === \"inline\" &&\n    currentFrame.shellInlineSize > 0 &&\n    Math.abs(\n      currentRestFrame.sidebarInlineSize - nextRestFrame.sidebarInlineSize,\n    ) > FILE_VIEWER_MOTION_EPSILON &&\n    resolvedTarget.durationMs > 0;\n\n  return {\n    currentRestFrame,\n    fromInlineSize,\n    nextRestFrame,\n    resolvedTarget,\n    shouldAnimate,\n  };\n}\n\nexport function areFileViewerMotionRestFramesEqual(\n  previous: FileViewerMotionRestFrame,\n  next: FileViewerMotionRestFrame,\n) {\n  return (\n    areFileViewerMotionNumbersEqual(\n      previous.shellInlineSize,\n      next.shellInlineSize,\n    ) &&\n    previous.durationMs === next.durationMs &&\n    areFileViewerMotionNumbersEqual(\n      previous.layoutInlineSize,\n      next.layoutInlineSize,\n    ) &&\n    previous.mode === next.mode &&\n    previous.open === next.open &&\n    previous.side === next.side &&\n    areFileViewerMotionNumbersEqual(\n      previous.sidebarInlineSize,\n      next.sidebarInlineSize,\n    ) &&\n    areFileViewerMotionNumbersEqual(previous.sidebarWidth, next.sidebarWidth)\n  );\n}\n\nexport function areFileViewerMotionFramesEqual(\n  previous: FileViewerMotionFrame,\n  next: FileViewerMotionFrame,\n) {\n  return (\n    areFileViewerMotionRestFramesEqual(previous, next) &&\n    previous.motionId === next.motionId &&\n    areFileViewerMotionNumbersEqual(\n      previous.motionProgress,\n      next.motionProgress,\n    ) &&\n    previous.phase === next.phase &&\n    areFileViewerMotionNumbersEqual(\n      previous.fromInlineSize,\n      next.fromInlineSize,\n    ) &&\n    areFileViewerMotionNumbersEqual(previous.toInlineSize, next.toInlineSize)\n  );\n}\n\nfunction pickFileViewerMotionRestFrame(\n  frame: FileViewerMotionFrame,\n): FileViewerMotionRestFrame {\n  return {\n    shellInlineSize: frame.shellInlineSize,\n    durationMs: frame.durationMs,\n    layoutInlineSize: frame.layoutInlineSize,\n    mode: frame.mode,\n    open: frame.open,\n    side: frame.side,\n    sidebarInlineSize: frame.sidebarInlineSize,\n    sidebarWidth: frame.sidebarWidth,\n  };\n}\n\nfunction resolveFileViewerMotionTarget({\n  currentFrame,\n  nextTarget,\n}: {\n  currentFrame: FileViewerMotionFrame;\n  nextTarget: FileViewerMotionTarget;\n}): FileViewerMotionTarget {\n  if (\n    nextTarget.mode !== \"overlay\" ||\n    currentFrame.mode !== \"inline\" ||\n    currentFrame.shellInlineSize <= 0\n  ) {\n    return nextTarget;\n  }\n\n  return {\n    ...nextTarget,\n    shellInlineSize:\n      nextTarget.shellInlineSize > 0\n        ? nextTarget.shellInlineSize\n        : currentFrame.shellInlineSize,\n    mode: \"inline\",\n  };\n}\n\nfunction areFileViewerMotionNumbersEqual(previous: number, next: number) {\n  return Math.abs(previous - next) <= 0.001;\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-motion-plan.ts"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-types.ts",
      "content": "import type * as React from \"react\";\n\nexport type ViewerSidebarMode = \"inline\" | \"overlay\";\nexport type ViewerSidebarRequestedMode = \"auto\" | ViewerSidebarMode;\nexport type ViewerSidebarGapTransition = \"width\" | \"none\";\nexport type ViewerSidebarState = \"expanded\" | \"collapsed\";\nexport type ViewerSidebarSide = \"left\" | \"right\";\nexport type ViewerSidebarCollapsible = \"offcanvas\" | \"none\";\nexport type ViewerDocumentFrameAlign = \"start\" | \"center\" | \"end\";\nexport type ViewerGeometryTransitionPhase = \"idle\" | \"sliding\";\n\nexport type ViewerDocumentReadingAnchorInput = {\n  scrollTop: number;\n  viewportBlockSize: number;\n};\n\nexport type ViewerDocumentReadingAnchorTarget<Anchor> = {\n  anchor: Anchor;\n  viewportBlockSize: number;\n};\n\nexport type ViewerDocumentTransitionSource =\n  | \"none\"\n  | \"viewer-shell\"\n  | \"document-layout\";\n\n// Commit-then-relax: layout commits its target inside the motion's first\n// frame and scroll rebases in the same commit, so there is no frozen layout\n// and no deferred scroll left in the vocabulary.\nexport type ViewerDocumentLayoutPolicy = \"live\" | \"target\";\nexport type ViewerDocumentScrollPolicy = \"preserve\" | \"rebase\";\nexport type ViewerDocumentVisualPolicy =\n  | \"none\"\n  | \"document-flip\"\n  | \"shell-transform\";\n\nexport type ViewerDocumentTransition = {\n  layoutPolicy: ViewerDocumentLayoutPolicy;\n  scrollPolicy: ViewerDocumentScrollPolicy;\n  source: ViewerDocumentTransitionSource;\n  transitionId: number | string | null;\n  visualPolicy: ViewerDocumentVisualPolicy;\n};\n\nexport type ViewerDocumentLayoutModel<Anchor> = {\n  blockSize: number;\n  captureReadingAnchor: (\n    input: ViewerDocumentReadingAnchorInput,\n  ) => Anchor | null;\n  getReadingAnchorScrollTop: (\n    target: ViewerDocumentReadingAnchorTarget<Anchor>,\n  ) => number | null;\n  inlineSize: number;\n  isTransitioning?: boolean;\n  transition?: ViewerDocumentTransition;\n};\n\nexport type ViewerDocumentPhysicalScrollPosition = {\n  physicalScrollTop: number;\n  scrollPageOffset: number;\n};\n\nexport type ViewerDocumentResolvedScrollTarget = {\n  left?: number;\n  top: number;\n};\n\nexport type ViewerDocumentScrollMapper = {\n  getLogicalScrollTop: (input: {\n    blockSize: number;\n    physicalScrollTop: number;\n    scrollPageOffset: number;\n    viewportBlockSize: number;\n  }) => number;\n  getPhysicalScrollSize: (input: {\n    blockSize: number;\n    viewportBlockSize: number;\n  }) => number;\n  resolvePhysicalScrollPosition: (input: {\n    blockSize: number;\n    logicalScrollTop: number;\n    scrollPageOffset: number;\n    viewportBlockSize: number;\n  }) => ViewerDocumentPhysicalScrollPosition;\n};\n\nexport type ViewerDocumentScrollMetrics = {\n  physicalScrollSize: number;\n  physicalScrollTop: number;\n  scrollPageOffset: number;\n  scrollTop: number;\n  viewportBlockSize: number;\n};\n\nexport type ViewerDocumentScrollTargetResolver<Anchor, Target> = (input: {\n  layout: ViewerDocumentLayoutModel<Anchor>;\n  scrollTop: number;\n  target: Target;\n  viewportElement: HTMLDivElement;\n}) => ViewerDocumentResolvedScrollTarget | null;\n\n// A zoom step is the one geometry change whose intent is \"zoom the camera\",\n// not \"keep my reading position\": it re-anchors the viewport CENTER on both\n// axes and relaxes a FLIP about that fixed point. `capture` runs in the zoom\n// gesture's own task against the pre-zoom layout and painted DOM;\n// `resolveScrollTarget` and `play` run inside the geometry commit against the\n// post-zoom layout (commit-then-relax).\nexport type ViewerDocumentZoomMotionBypassReason =\n  | \"resolve-failed\"\n  | \"shell-transition\"\n  | \"stale-intent\";\n\nexport type ViewerDocumentZoomMotionController<Transaction = unknown> = {\n  capture: (input: {\n    scrollTop: number;\n    viewportElement: HTMLDivElement;\n  }) => Transaction | null;\n  /**\n   * Telemetry tap: a captured zoom intent reached a geometry commit but the\n   * zoom lane declined it. Without this the bypass is invisible — the commit\n   * falls back to the reading-anchor restore and no flight is recorded.\n   */\n  noteBypass?: (reason: ViewerDocumentZoomMotionBypassReason) => void;\n  resolveScrollTarget: (input: {\n    transaction: Transaction;\n    viewportElement: HTMLDivElement;\n  }) => ViewerDocumentResolvedScrollTarget | null;\n  play: (input: {\n    transaction: Transaction;\n    viewportElement: HTMLDivElement;\n  }) => (() => void) | null;\n};\n\nexport type ViewerGeometrySnapshot = {\n  bodyInlineSize: number;\n  documentInlineSize: number;\n  hasMeasuredBody: boolean;\n  isTransitioning: boolean;\n  mode: ViewerSidebarMode;\n  open: boolean;\n  progress: number;\n  sidebarGapTransition: ViewerSidebarGapTransition;\n  sidebarInlineSize: number;\n  sidebarWidth: number;\n  side: ViewerSidebarSide;\n  state: ViewerSidebarState;\n  transitionPhase: ViewerGeometryTransitionPhase;\n};\n\nexport type ViewerGeometryStore = {\n  getSnapshot: () => ViewerGeometrySnapshot;\n  setTarget: (target: ViewerGeometryTarget) => void;\n  subscribe: (listener: () => void) => () => void;\n};\n\nexport type ViewerGeometryTarget = {\n  bodyElement: HTMLElement | null;\n  mode: ViewerSidebarMode;\n  open: boolean;\n  rootElement: HTMLElement | null;\n  sidebarElement: HTMLElement | null;\n  sidebarGapTransition: ViewerSidebarGapTransition;\n  sidebarWidth: number;\n  side: ViewerSidebarSide;\n  state: ViewerSidebarState;\n};\n\nexport type ViewerSidebarStateValue = {\n  state: ViewerSidebarState;\n  open: boolean;\n  setOpen: (value: boolean | ((open: boolean) => boolean)) => void;\n  toggleSidebar: () => void;\n  canToggleSidebar: boolean;\n  mode: ViewerSidebarMode;\n  side: ViewerSidebarSide;\n};\n\nexport type ViewerRootProps = React.ComponentProps<\"div\"> & {\n  defaultOpen?: boolean;\n  inlineBreakpoint?: number;\n  mode?: ViewerSidebarRequestedMode;\n  onOpenChange?: (open: boolean) => void;\n  open?: boolean;\n  sidebarCollapsible?: ViewerSidebarCollapsible;\n  sidebarGapTransition?: ViewerSidebarGapTransition;\n  sidebarSide?: ViewerSidebarSide;\n  stateNamespace?: ViewerStateAttributeNamespace;\n};\n\nexport type ViewerFrameProps = React.ComponentProps<\"div\">;\nexport type ViewerHeaderProps = React.ComponentProps<\"div\">;\nexport type ViewerBodyProps = React.ComponentProps<\"div\">;\nexport type ViewerSurfaceProps = React.ComponentProps<\"div\">;\nexport type ViewerViewportProps = React.ComponentProps<\"div\">;\nexport type ViewerDocumentFrameProps = React.ComponentProps<\"div\"> & {\n  align?: ViewerDocumentFrameAlign;\n  maxInlineSize?: React.CSSProperties[\"maxInlineSize\"];\n};\n\nexport type ViewerStateAttributeNamespace = {\n  prefix: string;\n  slots?: {\n    body?: boolean;\n    root?: boolean;\n    sidebar?: boolean;\n  };\n};\n\nexport type ViewerSidebarRegistration = {\n  collapsible: ViewerSidebarCollapsible;\n  element: HTMLElement;\n  id: string;\n  instanceId: string;\n  side: ViewerSidebarSide;\n  width: string;\n  widthPixels: number;\n};\n\nexport type ViewerPortalContainmentAttributes = {\n  \"data-viewer-portal-root-id\": string;\n};\n\nexport type ViewerSidebarRegistrationState = {\n  defaultSidebarCollapsible: ViewerSidebarCollapsible;\n  defaultSidebarSide: ViewerSidebarSide;\n  geometryStore: ViewerGeometryStore;\n  getRootElement: () => HTMLElement | null;\n  hasSidebar: boolean;\n  registerBody: (element: HTMLElement) => () => void;\n  registerSidebar: (registration: ViewerSidebarRegistration) => () => void;\n  rootId: string;\n  sidebarId: string;\n  sidebarGapTransition: ViewerSidebarGapTransition;\n  sidebarSide: ViewerSidebarSide;\n  setLastTriggerElement: (element: HTMLElement | null) => void;\n  stateNamespace?: ViewerStateAttributeNamespace;\n};\n\nexport type ViewerRootDiagnostics = {\n  getRootElement: () => HTMLElement | null;\n  layoutSignature: string;\n  rootId: string;\n};\n\nexport type ViewerSurfaceMeasurement = {\n  hasMeasured: boolean;\n  setViewportElement: React.RefCallback<HTMLDivElement>;\n  viewportElement: HTMLDivElement | null;\n  viewportHeight: number | null;\n  viewportWidth: number | null;\n};\n\nexport type ViewerSidebarSlotNames = {\n  container?: string;\n  gap?: string;\n  inner?: string;\n};\n\nexport type ViewerStateAttributeSlot = \"body\" | \"root\" | \"sidebar\";\nexport type ViewerStateAttributeValues = {\n  hasSidebar?: boolean;\n  sidebarCollapsible?: ViewerSidebarCollapsible;\n  sidebarMode?: ViewerSidebarMode;\n  sidebarOpen?: boolean;\n  sidebarSide?: ViewerSidebarSide;\n  sidebarState?: ViewerSidebarState;\n};\nexport type ViewerDataAttributes = Record<`data-${string}`, string | undefined>;\n\nexport type ViewerSidebarProps = React.ComponentProps<\"aside\"> &\n  ViewerDataAttributes & {\n    side?: ViewerSidebarSide;\n    collapsible?: ViewerSidebarCollapsible;\n    innerClassName?: string;\n    namespacedSlot?: string;\n    namespacedSlotNames?: ViewerSidebarSlotNames;\n    slotNames?: ViewerSidebarSlotNames;\n    width?: string;\n  };\n",
      "type": "registry:ui",
      "target": "@ui/viewer-types.ts"
    },
    {
      "path": "registry/new-york-v4/ui/use-reading-fraction-rebase.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\n// Preserve the reader's place across a fit-width resize.\n//\n// When the sidebar toggles, a document that fits its width re-fits to the new\n// inset: a wider page is a taller document, so its absolute scroll size changes\n// and a frozen scrollTop would drop the reader to a different place. The fix is\n// format-agnostic: continuously record the document fraction at the viewport\n// top, and the moment the layout changes, restore the viewport top to that\n// fraction of the new document — synchronously, before paint, so the visible\n// content never jumps.\n//\n// Renderers that already carry a richer per-page reading anchor (PDF, DOCX)\n// keep theirs; this is for the ones whose content simply scales with width\n// (image, PPTX, markdown), where the document fraction is the reading position.\nexport function useReadingFractionRebase({\n  scrollerRef,\n  layoutKey,\n  enabled = true,\n}: {\n  scrollerRef: React.RefObject<HTMLElement | null>;\n  // Anything that changes when the document re-fits (the fit width or scale).\n  // The rebase restores the captured fraction whenever this changes.\n  layoutKey: unknown;\n  enabled?: boolean;\n}) {\n  const fractionRef = React.useRef(0);\n  const committedKeyRef = React.useRef(layoutKey);\n\n  const captureReadingFraction = React.useCallback(() => {\n    const viewport = scrollerRef.current;\n    if (!viewport) return;\n    // The DOCUMENT fraction at the viewport top — never the fraction of the\n    // scroll range. Range fraction breaks down when the document barely\n    // overflows: 28px into a 28px range reads as \"scrolled to the bottom\",\n    // and restoring that bottom against a grown document teleports the\n    // camera to its middle. Content height is the linear coordinate the\n    // re-fit actually scales, so its fraction IS the reading position.\n    fractionRef.current =\n      viewport.scrollHeight > 0\n        ? viewport.scrollTop / viewport.scrollHeight\n        : 0;\n  }, [scrollerRef]);\n\n  useKeyedLayoutEffect(\n    joinEffectKey([\"reading-fraction-rebase\", layoutKey]),\n    () => {\n      const previousKey = committedKeyRef.current;\n      committedKeyRef.current = layoutKey;\n      if (!enabled) return;\n      if (Object.is(previousKey, layoutKey)) return;\n\n      const viewport = scrollerRef.current;\n      if (!viewport) return;\n      const range = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n      viewport.scrollTop = Math.min(\n        fractionRef.current * viewport.scrollHeight,\n        range,\n      );\n    },\n  );\n\n  return { captureReadingFraction };\n}\n",
      "type": "registry:ui",
      "target": "@ui/use-reading-fraction-rebase.ts"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-fit-width-motion.ts",
      "content": "\"use client\";\n\nimport type { FileViewerDocumentSurfaceMotionResolver } from \"./file-viewer-motion-kernel\";\nimport type { FileViewerMotionFrame } from \"./file-viewer-motion-plan\";\nimport type {\n  FileViewerDocumentAlign,\n  FileViewerInlineDirection,\n} from \"./file-viewer-renderer-contract\";\n\nexport const FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY =\n  \"--file-viewer-fit-width-anchor-block\";\n\n// Commit-then-relax: the renderer lays out at the motion's TARGET width from\n// the first commit, and this resolver reprojects that settled layout to the\n// in-flight visual width with one uniform transform. The transform terminates\n// on identity, so settle removes a no-op style instead of committing layout.\n//\n// The anchor custom property is the reading line's block offset in the settled\n// stage's own coordinates (post-rebase scrollTop + marker offset). It is read\n// live via var(), so the renderer writes it once per motion (in a layout\n// effect after the slide-start scroll rebase) without re-entering the kernel.\nexport function createFileViewerFitWidthSurfaceMotionResolver({\n  align,\n  anchorBlockProperty = FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY,\n  direction = \"ltr\",\n  isFitWidth,\n  stageInlineSize,\n  stageOuterInlinePadding = 0,\n  stageInlinePadding = 0,\n  stageInlineSlope = 1,\n  stageBlockSlope = stageInlineSlope,\n}: {\n  align: FileViewerDocumentAlign;\n  anchorBlockProperty?: string;\n  direction?: FileViewerInlineDirection;\n  isFitWidth: boolean;\n  stageInlineSize: number;\n  /** Constant symmetric padding around the transformed stage. */\n  stageOuterInlinePadding?: number;\n  /** Constant symmetric inline padding inside the transformed stage. */\n  stageInlinePadding?: number;\n  stageInlineSlope?: number;\n  /**\n   * Slope for the BLOCK axis when it differs from the inline one. A stage\n   * whose inline box carries constant padding while its block stack scales\n   * with the content (the image viewer: fit subtracts the horizontal\n   * padding, vertical gaps/padding scale) has two different affine models —\n   * X tracks the pane 1:1 while Y scales by the content ratio — and a\n   * uniform scale cannot land both axes exactly. Defaults to the inline\n   * slope (uniform scale) for fully proportional stages like the PDF.\n   */\n  stageBlockSlope?: number;\n}): FileViewerDocumentSurfaceMotionResolver {\n  return (frame) => {\n    if (!isFitWidth || frame.phase !== \"sliding\") {\n      return {\n        transform: \"\",\n        transformOrigin: \"\",\n        willChange: \"\",\n      };\n    }\n\n    return {\n      transform: getFileViewerFitWidthSurfaceMotionTransform({\n        align,\n        anchorBlockProperty,\n        direction,\n        frame,\n        stageInlineSize,\n        stageOuterInlinePadding,\n        stageInlinePadding,\n        stageInlineSlope,\n        stageBlockSlope,\n      }),\n      transformOrigin: \"0px 0px\",\n      willChange: \"transform\",\n    };\n  };\n}\n\n// Commit-then-relax for a CLAMPED reading column rather than a fit-width\n// stage: the stage's inline size is min(canvas, maxStageInlineSize), so it\n// does not scale with the pane — the only thing a width change moves is the\n// align margin. The canvas commits the motion's TARGET width from the first\n// sliding frame (minWidth under layoutPolicy \"target\"), which means a\n// widening pane's chunks land at the settled margin synchronously with the\n// click; this resolver reprojects them back to the live width's margin with\n// a translate that terminates on identity. A narrowing pane never engages it\n// (the canvas tracks the live width above its minWidth, so live and settled\n// margins agree) — exactly the leg that already glides on layout.\nexport function createFileViewerAlignTranslateSurfaceMotionResolver({\n  align,\n  direction = \"ltr\",\n  maxStageInlineSize,\n}: {\n  align: FileViewerDocumentAlign;\n  direction?: FileViewerInlineDirection;\n  /** The column's max inline size (the chunk's max-width, in px). */\n  maxStageInlineSize: number;\n}): FileViewerDocumentSurfaceMotionResolver {\n  return (frame) => {\n    if (frame.phase !== \"sliding\") {\n      return {\n        transform: \"\",\n        transformOrigin: \"\",\n        willChange: \"\",\n      };\n    }\n\n    return {\n      transform: getFileViewerAlignTranslateSurfaceMotionTransform({\n        align,\n        direction,\n        frame,\n        maxStageInlineSize,\n      }),\n      transformOrigin: \"0px 0px\",\n      willChange: \"transform\",\n    };\n  };\n}\n\nfunction getFileViewerAlignTranslateSurfaceMotionTransform({\n  align,\n  direction,\n  frame,\n  maxStageInlineSize,\n}: {\n  align: FileViewerDocumentAlign;\n  direction: FileViewerInlineDirection;\n  frame: FileViewerMotionFrame;\n  maxStageInlineSize: number;\n}) {\n  if (\n    !Number.isFinite(maxStageInlineSize) ||\n    maxStageInlineSize <= 0 ||\n    frame.layoutInlineSize <= 0 ||\n    frame.toInlineSize <= 0\n  ) {\n    return \"\";\n  }\n\n  // The canvas lays out at max(live, target): minWidth holds the committed\n  // target under a still-narrow pane, and a pane wider than the target just\n  // fills. The stage (reading column) centers/aligns INSIDE the canvas, and\n  // an overflowing canvas itself pins to the pane's start edge — left in\n  // LTR, right in RTL — so the stage's pane-space position carries the\n  // canvas offset too.\n  const canvasInlineSize = Math.max(frame.layoutInlineSize, frame.toInlineSize);\n  const stageInlineSize = Math.min(canvasInlineSize, maxStageInlineSize);\n  const canvasInlineOffset =\n    direction === \"rtl\"\n      ? Math.min(0, frame.layoutInlineSize - canvasInlineSize)\n      : 0;\n  const settledStageLeft =\n    canvasInlineOffset +\n    getFileViewerStageInlineMargin({\n      align,\n      availableInlineSize: canvasInlineSize,\n      direction,\n      stageInlineSize,\n    });\n  const liveStageLeft = getFileViewerStageInlineMargin({\n    align,\n    availableInlineSize: frame.layoutInlineSize,\n    direction,\n    stageInlineSize,\n  });\n  const translateX = liveStageLeft - settledStageLeft;\n\n  if (Math.abs(translateX) <= 0.001) return \"\";\n\n  return `translate3d(${formatFileViewerMotionPixel(translateX)}px, 0px, 0)`;\n}\n\nexport function getFileViewerFitWidthScale({\n  availableInlineSize,\n  contentInlineSize,\n  stageInlinePadding = 0,\n}: {\n  availableInlineSize: number;\n  contentInlineSize: number;\n  stageInlinePadding?: number;\n}) {\n  if (availableInlineSize <= 0 || contentInlineSize <= 0) return 1;\n\n  const contentAvailableInlineSize = Math.max(\n    1,\n    availableInlineSize - stageInlinePadding,\n  );\n  return contentAvailableInlineSize / contentInlineSize;\n}\n\n// The visual scale the resolver renders for a given live width — the same\n// affine reprojection as the transform itself. Renderers use it to reason\n// about the on-screen state (anchor capture/solve) without duplicating the\n// formula.\n//\n// stageInlineSlope is how many stage pixels the settled stage grows per pane\n// pixel. It is 1 whenever the stage IS the fit-width content (image, docx,\n// pptx, uniform-width PDFs: stage = pane − constant padding), but a stage\n// that is WIDER than its fit basis grows faster than the pane — a PDF fits\n// its FIRST page while the stage spans its WIDEST page, so a mixed-width\n// document has slope maxBase/fitBase > 1. A unit-slope assumption there\n// under-scales the first frame by (slope − 1)·delta/stage — measured as a\n// ~7px content step at the anchor-hold frame of a 355-page prospectus.\nexport function getFileViewerFitWidthVisualScale({\n  liveInlineSize,\n  stageInlineSize,\n  stageInlineSlope = 1,\n  targetInlineSize,\n}: {\n  liveInlineSize: number;\n  stageInlineSize: number;\n  stageInlineSlope?: number;\n  targetInlineSize: number;\n}) {\n  if (\n    stageInlineSize <= 0 ||\n    !Number.isFinite(liveInlineSize) ||\n    !Number.isFinite(targetInlineSize)\n  ) {\n    return 1;\n  }\n  const slope =\n    Number.isFinite(stageInlineSlope) && stageInlineSlope > 0\n      ? stageInlineSlope\n      : 1;\n  return (\n    Math.max(1, stageInlineSize + slope * (liveInlineSize - targetInlineSize)) /\n    stageInlineSize\n  );\n}\n\n// Capture side of the motion anchor: the probe content line's on-screen block\n// offset relative to the scroll box, taken just before a motion (or retarget)\n// commits. When a motion is already in flight the DOM is the settled layout\n// PLUS the live transform, so the capture applies that transform — otherwise\n// a retarget would solve continuity against a picture the reader never saw.\nexport function captureFileViewerFitWidthAnchorScreenOffset({\n  lastAnchorBlock,\n  liveFrame,\n  probeStageOffset,\n  scrollTop,\n  stageInlineSize,\n  stageInlinePadding = 0,\n  stageBlockSlope = 1,\n}: {\n  lastAnchorBlock: number | null;\n  liveFrame: FileViewerMotionFrame | null;\n  probeStageOffset: number;\n  scrollTop: number;\n  stageInlineSize: number;\n  stageInlinePadding?: number;\n  /** The BLOCK-axis slope — anchor capture/solve is block-axis math. */\n  stageBlockSlope?: number;\n}) {\n  const untransformed = probeStageOffset - scrollTop;\n  if (!liveFrame || liveFrame.phase !== \"sliding\") return untransformed;\n\n  const liveScale = getFileViewerFitWidthVisualScale({\n    liveInlineSize: liveFrame.layoutInlineSize,\n    stageInlineSize: getFileViewerFitWidthContentInlineSize({\n      stageInlinePadding,\n      stageInlineSize,\n    }),\n    stageInlineSlope: stageBlockSlope,\n    targetInlineSize: liveFrame.toInlineSize,\n  });\n  if (Math.abs(1 - liveScale) <= 0.001) return untransformed;\n\n  return (\n    liveScale * probeStageOffset +\n    (1 - liveScale) * (lastAnchorBlock ?? 0) -\n    scrollTop\n  );\n}\n\n// Solve side: the anchor block offset that puts the probe content line back on\n// its captured screen position under the NEW layout model at the motion's\n// first-frame scale. Exact regardless of how the rebase clamped or how the\n// old/new layout models relate (measured page sizes, constant gaps/padding).\n// Returns null when the motion is degenerate (caller falls back to the live\n// reading marker).\nexport function resolveFileViewerFitWidthMotionAnchorBlock({\n  fromInlineSize,\n  probeScreenOffset,\n  probeStageOffset,\n  scrollTop,\n  stageInlineSize,\n  stageInlinePadding = 0,\n  stageBlockSlope = 1,\n  toInlineSize,\n}: {\n  fromInlineSize: number | null;\n  probeScreenOffset: number;\n  probeStageOffset: number;\n  scrollTop: number;\n  stageInlineSize: number;\n  stageInlinePadding?: number;\n  /** The BLOCK-axis slope — anchor capture/solve is block-axis math. */\n  stageBlockSlope?: number;\n  toInlineSize: number | null;\n}) {\n  if (fromInlineSize == null || toInlineSize == null) return null;\n\n  const startScale = getFileViewerFitWidthVisualScale({\n    liveInlineSize: fromInlineSize,\n    stageInlineSize: getFileViewerFitWidthContentInlineSize({\n      stageInlinePadding,\n      stageInlineSize,\n    }),\n    stageInlineSlope: stageBlockSlope,\n    targetInlineSize: toInlineSize,\n  });\n  if (!Number.isFinite(startScale) || Math.abs(1 - startScale) <= 0.001) {\n    return null;\n  }\n\n  return (\n    (probeScreenOffset + scrollTop - startScale * probeStageOffset) /\n    (1 - startScale)\n  );\n}\n\nfunction getFileViewerFitWidthSurfaceMotionTransform({\n  align,\n  anchorBlockProperty,\n  direction,\n  frame,\n  stageInlineSize,\n  stageOuterInlinePadding,\n  stageInlinePadding,\n  stageInlineSlope,\n  stageBlockSlope,\n}: {\n  align: FileViewerDocumentAlign;\n  anchorBlockProperty: string;\n  direction: FileViewerInlineDirection;\n  frame: FileViewerMotionFrame;\n  stageInlineSize: number;\n  stageOuterInlinePadding: number;\n  stageInlinePadding: number;\n  stageInlineSlope: number;\n  stageBlockSlope: number;\n}) {\n  if (\n    stageInlineSize <= 0 ||\n    frame.layoutInlineSize <= 0 ||\n    frame.toInlineSize <= 0\n  ) {\n    return \"\";\n  }\n\n  // Fit-width renderers size their stage as an affine function of the\n  // available width (stage = slope × width − constant padding), so the\n  // in-flight visual stage is the settled stage plus the scaled live width\n  // delta. At the first frame this resolves to exactly the pre-toggle stage\n  // size, and at the last frame to the settled stage — identity. Each axis\n  // carries its own slope: they differ when the stage's inline box holds\n  // constant padding while its block stack scales with the content.\n  const contentInlineSize = getFileViewerFitWidthContentInlineSize({\n    stageInlinePadding,\n    stageInlineSize,\n  });\n  const inlineScale = getFileViewerFitWidthVisualScale({\n    liveInlineSize: frame.layoutInlineSize,\n    stageInlineSize: contentInlineSize,\n    stageInlineSlope,\n    targetInlineSize: frame.toInlineSize,\n  });\n  const blockScale = getFileViewerFitWidthVisualScale({\n    liveInlineSize: frame.layoutInlineSize,\n    stageInlineSize: contentInlineSize,\n    stageInlineSlope: stageBlockSlope,\n    targetInlineSize: frame.toInlineSize,\n  });\n  const visualStageInlineSize =\n    inlineScale * contentInlineSize + stageInlinePadding;\n  const availableStageInlineSize = Math.max(\n    1,\n    frame.layoutInlineSize - Math.max(0, stageOuterInlinePadding),\n  );\n  const settledMargin = getFileViewerStageInlineMargin({\n    align,\n    availableInlineSize: availableStageInlineSize,\n    direction,\n    stageInlineSize,\n  });\n  const visualMargin = getFileViewerStageInlineMargin({\n    align,\n    availableInlineSize: availableStageInlineSize,\n    direction,\n    stageInlineSize: visualStageInlineSize,\n  });\n  // The padding is constant in both endpoint layouts. Scaling the outer stage\n  // would scale that inset too, making the visible page briefly too wide or\n  // narrow on the first frame. Rebase the symmetric start inset so the inner\n  // content edge, not the transparent wrapper edge, is pixel-continuous.\n  const inlinePaddingStart = stageInlinePadding / 2;\n  const translateX =\n    visualMargin - settledMargin + (1 - inlineScale) * inlinePaddingStart;\n\n  if (Math.abs(frame.layoutInlineSize - frame.toInlineSize) <= 0.001) {\n    return \"\";\n  }\n\n  const formattedInlineScale = formatFileViewerMotionScale(inlineScale);\n  const formattedBlockScale = formatFileViewerMotionScale(blockScale);\n  const formattedTranslateX = formatFileViewerMotionPixel(translateX);\n  // Scale about the stage origin; the anchor term keeps the reading line\n  // fixed on the block axis: y' = s·y + (1 − s)·anchor equals y at\n  // y = anchor.\n  const translateY = `calc((1 - ${formattedBlockScale}) * var(${anchorBlockProperty}, 0px))`;\n  const formattedScale =\n    formattedInlineScale === formattedBlockScale\n      ? formattedInlineScale\n      : `${formattedInlineScale}, ${formattedBlockScale}`;\n\n  return `translate3d(${formattedTranslateX}px, ${translateY}, 0) scale(${formattedScale})`;\n}\n\nfunction getFileViewerFitWidthContentInlineSize({\n  stageInlinePadding,\n  stageInlineSize,\n}: {\n  stageInlinePadding: number;\n  stageInlineSize: number;\n}) {\n  const padding = Number.isFinite(stageInlinePadding)\n    ? Math.max(0, stageInlinePadding)\n    : 0;\n  return Math.max(1, stageInlineSize - padding);\n}\n\n// Physical LEFT offset of the stage box inside the available inline size —\n// translateX shifts along the physical X axis, so the model must speak\n// physical-left in both directions. Stages align with physical auto margins\n// (mx-auto for center, ml-auto for end, plain flow for start), so:\n// - free space ≥ 0: center splits it; end pins right in both directions\n//   (ml-auto is physical); start follows flow (left in LTR, right in RTL).\n// - free space < 0 (the settled stage overflows the live container — the\n//   close leg's early frames): auto margins collapse to 0 and CSS resolves\n//   the over-constraint against the direction's end edge, pinning the box to\n//   the start edge — left edge at 0 in LTR, at the negative free space in\n//   RTL. The old unconditional max(0, …) clamp encoded only the LTR half and\n//   made the RTL close leg overshoot by the overflow amount.\nfunction getFileViewerStageInlineMargin({\n  align,\n  availableInlineSize,\n  direction,\n  stageInlineSize,\n}: {\n  align: FileViewerDocumentAlign;\n  availableInlineSize: number;\n  direction: FileViewerInlineDirection;\n  stageInlineSize: number;\n}) {\n  const freeInlineSize = availableInlineSize - stageInlineSize;\n  if (freeInlineSize < 0) return direction === \"rtl\" ? freeInlineSize : 0;\n\n  switch (align) {\n    case \"start\":\n      return direction === \"rtl\" ? freeInlineSize : 0;\n    case \"end\":\n      return freeInlineSize;\n    case \"center\":\n      return freeInlineSize / 2;\n  }\n}\n\nfunction formatFileViewerMotionPixel(value: number) {\n  return Number.isFinite(value) ? Number(value.toFixed(3)) : 0;\n}\n\nfunction formatFileViewerMotionScale(value: number) {\n  return Number.isFinite(value) ? String(Number(value.toFixed(6))) : \"1\";\n}\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-fit-width-motion.ts"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-measurement.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport type StableElementSize<Element extends HTMLElement = HTMLElement> = {\n  element: Element | null;\n  hasMeasured: boolean;\n  height: number | null;\n  setElement: React.RefCallback<Element>;\n  width: number | null;\n};\n\nexport type StableElementSizeOptions = {\n  enabled?: boolean;\n  observe?: boolean;\n  retainLastNonZero?: boolean;\n};\n\nexport type StableCssLengthOptions = {\n  element: HTMLElement | null;\n  retainLastNonZero?: boolean;\n  value: string;\n};\n\ntype MeasuredSize = {\n  height: number | null;\n  width: number | null;\n};\n\ntype RawMeasuredSize = {\n  height: number;\n  width: number;\n};\n\n// DOM layout reads for viewer chrome are quarantined in this module. The\n// file-viewer motion kernel (time + style writes only) receives this reader by\n// injection from the frame controller instead of touching layout APIs itself.\nexport function readElementRectSnapshot(\n  element: HTMLElement | null,\n): readonly number[] {\n  if (!element) return [];\n  const rect = element.getBoundingClientRect();\n  return [rect.left, rect.top, rect.width, rect.height];\n}\n\n// Computed CSS inline direction of an element, sampled when it attaches (a\n// runtime `dir` flip is picked up on the next mount). The fit-width motion\n// transform works on the physical X axis, so the renderer frame needs to\n// know which edge auto-margin alignment pins the stage to.\nexport function useViewerInlineDirection(\n  element: HTMLElement | null,\n): \"ltr\" | \"rtl\" {\n  const [direction, setDirection] = React.useState<\"ltr\" | \"rtl\">(\"ltr\");\n\n  useKeyedLayoutEffect(element ? joinEffectKey([element]) : null, () => {\n    if (!element) return;\n    setDirection(getComputedStyle(element).direction === \"rtl\" ? \"rtl\" : \"ltr\");\n  });\n\n  return direction;\n}\n\nfunction readElementSize(element: HTMLElement): RawMeasuredSize {\n  const rect =\n    typeof element.getBoundingClientRect === \"function\"\n      ? element.getBoundingClientRect()\n      : null;\n\n  return {\n    height: rect?.height || element.clientHeight,\n    width: rect?.width || element.clientWidth,\n  };\n}\n\nfunction resolveMeasuredElementSize({\n  currentSize,\n  nextSize,\n  retainLastNonZero,\n}: {\n  currentSize: MeasuredSize;\n  nextSize: RawMeasuredSize;\n  retainLastNonZero: boolean;\n}): MeasuredSize {\n  const width =\n    Number.isFinite(nextSize.width) &&\n    (!retainLastNonZero || nextSize.width > 0)\n      ? nextSize.width\n      : currentSize.width;\n  const height =\n    Number.isFinite(nextSize.height) &&\n    (!retainLastNonZero || nextSize.height > 0)\n      ? nextSize.height\n      : currentSize.height;\n\n  if (currentSize.width === width && currentSize.height === height) {\n    return currentSize;\n  }\n\n  return { height, width };\n}\n\nexport function useStableElementSize<Element extends HTMLElement = HTMLElement>(\n  options: StableElementSizeOptions = {},\n): StableElementSize<Element> {\n  const enabled = options.enabled ?? true;\n  const observe = options.observe ?? true;\n  const retainLastNonZero = options.retainLastNonZero ?? false;\n  const [element, setElementState] = React.useState<Element | null>(null);\n  const [size, setSize] = React.useState<MeasuredSize>({\n    height: null,\n    width: null,\n  });\n  const hasMeasured = size.height !== null || size.width !== null;\n\n  const setElement = React.useCallback((nextElement: Element | null) => {\n    setElementState(nextElement);\n  }, []);\n\n  useKeyedLayoutEffect(enabled ? null : \"reset\", () => {\n    setSize({ height: null, width: null });\n  });\n\n  useKeyedLayoutEffect(\n    enabled && element\n      ? joinEffectKey([element, observe, retainLastNonZero])\n      : null,\n    () => {\n      if (!element) return;\n\n      setSize((currentSize) =>\n        resolveMeasuredElementSize({\n          currentSize,\n          nextSize: readElementSize(element),\n          retainLastNonZero,\n        }),\n      );\n\n      const ResizeObserverConstructor = observe\n        ? globalThis.ResizeObserver\n        : undefined;\n      if (typeof ResizeObserverConstructor === \"undefined\") return;\n\n      let frame = 0;\n      let latestSize = readElementSize(element);\n      const observer = new ResizeObserverConstructor((entries) => {\n        for (const entry of entries) {\n          latestSize = readElementSize(entry.target as HTMLElement);\n        }\n\n        if (frame) return;\n        frame = requestAnimationFrame(() => {\n          frame = 0;\n          setSize((currentSize) =>\n            resolveMeasuredElementSize({\n              currentSize,\n              nextSize: latestSize,\n              retainLastNonZero,\n            }),\n          );\n        });\n      });\n\n      observer.observe(element);\n\n      return () => {\n        if (frame) cancelAnimationFrame(frame);\n        observer.disconnect();\n      };\n    },\n  );\n\n  return React.useMemo(\n    () => ({\n      element,\n      hasMeasured,\n      height: size.height,\n      setElement,\n      width: size.width,\n    }),\n    [element, hasMeasured, setElement, size.height, size.width],\n  );\n}\n\nexport function useStableCssLength({\n  element,\n  retainLastNonZero = true,\n  value,\n}: StableCssLengthOptions) {\n  const [resolvedLength, setResolvedLength] = React.useState(0);\n\n  useKeyedLayoutEffect(\n    value ? joinEffectKey([element, retainLastNonZero, value]) : null,\n    () => {\n      const nextLength = resolveCssLength(value, element);\n\n      setResolvedLength((currentLength) => {\n        if (retainLastNonZero && nextLength <= 0) return currentLength;\n        return areCssLengthsEqual(currentLength, nextLength)\n          ? currentLength\n          : nextLength;\n      });\n    },\n  );\n\n  return resolvedLength;\n}\n\nfunction resolveCssLength(value: string, element: HTMLElement | null) {\n  const trimmedValue = value.trim();\n  const pixelMatch = trimmedValue.match(/^(-?\\d+(?:\\.\\d+)?)px$/);\n  if (pixelMatch) return Math.max(0, Number(pixelMatch[1]));\n\n  if (typeof window === \"undefined\") return 0;\n\n  const remMatch = trimmedValue.match(/^(-?\\d+(?:\\.\\d+)?)rem$/);\n  if (remMatch) {\n    return (\n      Math.max(0, Number(remMatch[1])) *\n      readComputedFontSize(window.document.documentElement)\n    );\n  }\n\n  const emMatch = trimmedValue.match(/^(-?\\d+(?:\\.\\d+)?)em$/);\n  if (emMatch) {\n    return Math.max(0, Number(emMatch[1])) * readComputedFontSize(element);\n  }\n\n  const measuringElement = window.document.createElement(\"div\");\n  measuringElement.style.contain = \"strict\";\n  measuringElement.style.inlineSize = trimmedValue;\n  measuringElement.style.position = \"absolute\";\n  measuringElement.style.visibility = \"hidden\";\n  (element ?? window.document.body).appendChild(measuringElement);\n  const width = readElementInlineSize(measuringElement);\n  measuringElement.remove();\n  return width;\n}\n\nfunction readElementInlineSize(element: HTMLElement) {\n  const rect =\n    typeof element.getBoundingClientRect === \"function\"\n      ? element.getBoundingClientRect()\n      : null;\n  const width = rect?.width || element.clientWidth || 0;\n  return Number.isFinite(width) && width > 0 ? width : 0;\n}\n\nfunction readComputedFontSize(element: Element | null) {\n  if (typeof window === \"undefined\") return 16;\n  const fontSize = element ? window.getComputedStyle(element).fontSize : \"16px\";\n  const value = Number.parseFloat(fontSize);\n  return Number.isFinite(value) && value > 0 ? value : 16;\n}\n\nfunction areCssLengthsEqual(previous: number, next: number) {\n  return Math.abs(previous - next) <= 0.001;\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-measurement.ts"
    }
  ],
  "type": "registry:ui"
}