{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-viewer",
  "title": "Image Viewer",
  "description": "A performant image viewer with native off-thread decode (createImageBitmap), continuous scroll, zoom, rotate, fit-to-width, download, and a per-frame overlay slot. Renders multi-page TIFF scans efficiently: frames decode lazily near the viewport and decoded bitmaps are capped. No useEffect.",
  "dependencies": [
    "lucide-react",
    "utif@3.1.0"
  ],
  "registryDependencies": [
    "@retab/utils",
    "button",
    "@retab/scroll-area",
    "separator",
    "@retab/skeleton",
    "@retab/viewer-controls",
    "@retab/use-mount-effect",
    "@retab/use-keyed-layout-effect"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/image-viewer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n  createFrameSource,\n  isTiffBytes,\n  type FrameDescriptor,\n  type FrameSource,\n} from \"@/lib/image-frame-source\";\nimport {\n  createViewerResource,\n  type ViewerResource,\n} from \"@/lib/viewer-resource\";\nimport { ImageViewerFallback } from \"@/components/ui/image-viewer-chrome\";\nimport {\n  getImageSource,\n  ImageViewerContent,\n  resetImageSourceCacheForTests,\n} from \"@/components/ui/image-viewer-content\";\nimport type {\n  ImageViewerHandle,\n  ImageViewerProps,\n} from \"@/components/ui/image-viewer-types\";\nimport { useIsClient } from \"@/components/ui/use-is-client\";\nimport { ViewerErrorBoundary } from \"@/components/ui/viewer-error\";\n\nexport type {\n  ImageDocumentSource,\n  ImageFrameOverlayProps,\n  ImageFrameRenderTiming,\n  ImageViewerHandle,\n  ImageViewerProps,\n} from \"@/components/ui/image-viewer-types\";\nexport type { FrameDescriptor, FrameSource };\nexport { getImageSource, resetImageSourceCacheForTests };\n\nexport type ImageResourceContentProps = Omit<ImageViewerProps, \"source\"> & {\n  resource: ViewerResource;\n};\n\nexport type ImageViewerProviderProps = {\n  children: React.ReactNode;\n  resource: ViewerResource;\n};\n\nexport type ImageViewerFramesProps = Omit<\n  ImageResourceContentProps,\n  \"resource\"\n>;\n\nexport const ImageViewer = React.forwardRef<\n  ImageViewerHandle,\n  ImageViewerProps\n>(function ImageViewer(props, ref) {\n  const { source, ...resourceProps } = props;\n  const resource = React.useMemo(() => createViewerResource(source), [source]);\n  return (\n    <ImageResourceContent {...resourceProps} ref={ref} resource={resource} />\n  );\n});\n\nexport const ImageResourceContent = React.forwardRef<\n  ImageViewerHandle,\n  ImageResourceContentProps\n>(function ImageResourceContent(props, ref) {\n  const isClient = useIsClient();\n  const resource = props.resource;\n  if (!isClient) {\n    return (\n      <ImageViewerFallback\n        bare={props.bare}\n        className={props.className}\n        fallbackFrameSize={props.fallbackFrameSize}\n        scale={props.scale ?? props.defaultScale}\n        controls={props.controls}\n      />\n    );\n  }\n  return (\n    <ViewerErrorBoundary\n      className={props.className}\n      download={\n        props.controls === false || props.download === false\n          ? null\n          : resource.originalDownload\n      }\n      format=\"image\"\n      resetKey={resource.keys.resource}\n      sourceKind={resource.sourceKind}\n    >\n      <React.Suspense\n        fallback={\n          <ImageViewerFallback\n            bare={props.bare}\n            className={props.className}\n            fallbackFrameSize={props.fallbackFrameSize}\n            scale={props.scale ?? props.defaultScale}\n            controls={props.controls}\n          />\n        }\n      >\n        <ImageViewerContent {...props} forwardedRef={ref} resource={resource} />\n      </React.Suspense>\n    </ViewerErrorBoundary>\n  );\n});\n\nconst ImageViewerResourceContext = React.createContext<ViewerResource | null>(\n  null,\n);\n\nexport function ImageViewerProvider({\n  children,\n  resource,\n}: ImageViewerProviderProps) {\n  return (\n    <ImageViewerResourceContext.Provider value={resource}>\n      {children}\n    </ImageViewerResourceContext.Provider>\n  );\n}\n\nfunction useImageViewerResource(): ViewerResource {\n  const resource = React.useContext(ImageViewerResourceContext);\n  if (!resource) {\n    throw new Error(\n      \"ImageViewerFrames must be used within ImageViewerProvider.\",\n    );\n  }\n  return resource;\n}\n\nexport const ImageViewerFrames = React.forwardRef<\n  ImageViewerHandle,\n  ImageViewerFramesProps\n>(function ImageViewerFrames(props, ref) {\n  const resource = useImageViewerResource();\n  return <ImageResourceContent {...props} ref={ref} resource={resource} />;\n});\n\nexport function looksLikeTiff(\n  src: string,\n  contentType: string | null,\n  bytes: ArrayBuffer,\n): boolean {\n  return isTiffBytes(src, contentType, bytes);\n}\n\nexport function createImageSourceForTests(\n  kind: \"image\" | \"tiff\",\n  frames: readonly { width: number; height: number }[],\n  decode: (frameIndex: number) => Promise<ImageBitmap>,\n  onDispose?: (reason: Error) => void,\n): FrameSource {\n  return createFrameSource({\n    kind: kind === \"image\" ? \"native-image\" : \"tiff\",\n    frames: frames.map((frame) => ({\n      intrinsicSize: { width: frame.width, height: frame.height },\n    })),\n    decode,\n    maxDecodedFrames: 16,\n    onDispose,\n  });\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/file-viewer-core.ts",
      "content": "import {\n  detectCategory,\n  extensionOf,\n  extractName,\n  resolveViewerDescriptor,\n  textPayloadKey,\n  type FileCategory,\n  type ViewerDescriptor,\n  type ViewerSource,\n} from \"@/lib/viewer-source\";\n\nexport type { FileCategory, ViewerSource };\nexport type FileViewerFallbackSize = { width: number; height: number };\n\nexport interface FileViewerCoreProps {\n  source: ViewerSource;\n  category?: FileCategory;\n  className?: string;\n  /** Intrinsic first-frame size for image/TIFF loading skeletons. */\n  fallbackFrameSize?: FileViewerFallbackSize;\n  /** Intrinsic first-slide size for PPTX loading skeletons. */\n  fallbackSlideSize?: FileViewerFallbackSize;\n  isolateStyles?: boolean;\n}\n\nexport type FileDescriptor = ViewerDescriptor;\n\nexport function resolveFileDescriptor({\n  source,\n  category,\n}: FileViewerCoreProps): FileDescriptor {\n  return resolveViewerDescriptor({\n    source,\n    category,\n  });\n}\n\nexport function descriptorResetKey(descriptor: FileDescriptor): string {\n  return [\n    descriptorIdentityResetKey(descriptor),\n    descriptor.displayName,\n    descriptor.mimeType ?? \"\",\n    descriptor.category,\n  ].join(\"\\u0000\");\n}\n\nfunction descriptorIdentityResetKey(descriptor: FileDescriptor): string {\n  if (\n    descriptor.source.kind === \"text\" &&\n    descriptor.source.identityKey == null\n  ) {\n    return textPayloadKey(descriptor.source.text);\n  }\n  return descriptor.identityKey;\n}\n\nexport function isProseTextDescriptor(descriptor: FileDescriptor): boolean {\n  if (descriptor.category !== \"text\") return false;\n\n  const extension = extensionOf(descriptor.fileName);\n  if (extension === \"txt\" || extension === \"text\") return true;\n  return !extension && descriptor.mimeType?.toLowerCase() === \"text/plain\";\n}\n\nexport { detectCategory, extensionOf, extractName };\n",
      "type": "registry:ui",
      "target": "@ui/file-viewer-core.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/lib/viewer-download-actions.ts",
      "content": "export type ViewerDownloadOrigin = \"original\" | \"derived\";\n\nexport type ViewerDownloadPayload =\n  | { kind: \"href\"; href: string }\n  | { kind: \"blob\"; blob: Blob }\n  | { kind: \"text\"; text: string; mimeType?: string }\n  | { kind: \"none\" };\n\nexport interface ViewerDownloadAction {\n  id: string;\n  label: string;\n  fileName: string;\n  origin: ViewerDownloadOrigin;\n  isDisabled?: boolean;\n  getPayload: (options?: {\n    signal?: AbortSignal;\n  }) => ViewerDownloadPayload | Promise<ViewerDownloadPayload>;\n}\n\nexport type ViewerDownloadErrorKind =\n  | \"disabled\"\n  | \"aborted\"\n  | \"payload_failed\"\n  | \"unsupported\";\n\nexport class ViewerDownloadError extends Error {\n  readonly kind: ViewerDownloadErrorKind;\n  readonly actionId: string;\n  override readonly cause?: unknown;\n\n  constructor({\n    actionId,\n    kind,\n    message,\n    cause,\n  }: {\n    actionId: string;\n    kind: ViewerDownloadErrorKind;\n    message: string;\n    cause?: unknown;\n  }) {\n    super(message);\n    this.name = \"ViewerDownloadError\";\n    this.actionId = actionId;\n    this.kind = kind;\n    this.cause = cause;\n  }\n}\n\nexport function createHrefDownloadAction({\n  id,\n  label = \"Download\",\n  href,\n  fileName,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  href: string;\n  fileName: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    getPayload: () => ({ kind: \"href\", href }),\n  };\n}\n\nexport function createBlobDownloadAction({\n  id,\n  label = \"Download\",\n  blob,\n  fileName,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  blob: Blob;\n  fileName: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    getPayload: () => ({ kind: \"blob\", blob }),\n  };\n}\n\nexport function createTextDownloadAction({\n  id,\n  label = \"Download\",\n  text,\n  fileName,\n  mimeType,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  text: string;\n  fileName: string;\n  mimeType?: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    getPayload: () => ({ kind: \"text\", text, mimeType }),\n  };\n}\n\nexport function createDisabledDownloadAction({\n  id,\n  label = \"Download\",\n  fileName,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  fileName: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    isDisabled: true,\n    getPayload: () => ({ kind: \"none\" }),\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/viewer-download-actions.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/image-viewer-types.ts",
      "content": "import type * as React from \"react\";\n\nimport type { BlobViewerSource, UrlViewerSource } from \"@/lib/viewer-source\";\n\nexport interface ImageFrameOverlayProps {\n  /** 1-based frame index (a TIFF page; always 1 for single images). */\n  frameNumber: number;\n  /** Rendered frame size in CSS pixels (post-scale, post-rotation). */\n  width: number;\n  height: number;\n  scale: number;\n  rotation: number;\n}\n\nexport interface ImageFrameRenderTiming {\n  cached: boolean;\n  durationMs: number;\n  frameNumber: number;\n  pixelRatio: number;\n  renderScale: number;\n  status: \"rendered\" | \"cancelled\" | \"failed\";\n}\n\n/**\n * Imperative handle for driving the viewer from outside (e.g. scroll to the\n * source of a hovered field). Obtain it with a `ref` on `<ImageViewer>`.\n */\nexport interface ImageViewerHandle {\n  /**\n   * Scroll a frame's normalized area into view. `area` fields are percentages\n   * [0, 100] of the rendered frame; only `top` is required. Pass\n   * `behavior: \"auto\"` for an instant jump (e.g. on hover).\n   */\n  scrollToFrameArea: (\n    frameNumber: number,\n    area: { top: number; left?: number; width?: number; height?: number },\n    options?: ScrollToOptions,\n  ) => void;\n  /** The scrolling viewport element, or null before the image loads. */\n  getViewportElement: () => HTMLDivElement | null;\n}\n\nexport type ImageDocumentSource = UrlViewerSource | BlobViewerSource;\n\nexport interface ImageViewerProps {\n  /** Canonical image source. PNG/JPEG/WebP/GIF/AVIF/BMP/ICO or TIFF. */\n  source: ImageDocumentSource;\n  className?: string;\n  /** Fixed scale; when omitted the viewer fits frame width to the container. */\n  scale?: number;\n  /** Initial uncontrolled scale. When omitted, uncontrolled mode starts fit-width. */\n  defaultScale?: number;\n  /** Intrinsic size used to reserve the first frame while image metadata loads. */\n  fallbackFrameSize?: { 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 (e.g. bbox citations) on each frame. */\n  renderFrameOverlay?: (props: ImageFrameOverlayProps) => React.ReactNode;\n  /** Reports measured canvas render work for benchmark and profiling surfaces. */\n  onFrameRenderTiming?: (timing: ImageFrameRenderTiming) => void;\n  /** Fired with the 1-based frame nearest the top of the viewport as you scroll. */\n  onVisibleFrameChange?: (frameNumber: number) => void;\n  /** Fired with scroll progress in [0, 1] (for a fine-grained scroll cursor). */\n  onScrollProgressChange?: (progress: number) => void;\n  /** Drop the outer border/rounded/background so the viewer fills its container. */\n  bare?: boolean;\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer-types.ts"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer-content.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { type FrameSource } from \"@/lib/image-frame-source\";\nimport {\n  imageFrameSourceManager,\n  type FrameSourceLease,\n  type ImageSourceContent,\n} from \"@/lib/image-source-cache\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type ViewerContentIdentity,\n  type ViewerResource,\n} from \"@/lib/viewer-resource\";\nimport { ImageFrameScroller } from \"@/components/ui/image-viewer-frame\";\nimport {\n  IMAGE_READING_MARKER_RATIO,\n  IMAGE_VIEWER_HORIZONTAL_PADDING,\n  MAX_VIEWER_SCALE,\n  MIN_VIEWER_SCALE,\n  useFrameListWidth,\n  useImageViewerHandle,\n  useImageViewerScale,\n  useVisibleFrame,\n} from \"@/components/ui/image-viewer-hooks\";\nimport {\n  type ImageViewerHandle,\n  type ImageViewerProps,\n} from \"@/components/ui/image-viewer-types\";\nimport {\n  useViewerControlsRegistration,\n  ViewerControls,\n  type ViewerControlsState,\n} from \"@/components/ui/viewer-controls\";\n\nimport {\n  FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,\n  readFileViewerBeforeLayoutMotionFrame,\n} from \"./file-viewer-elements\";\nimport type { FileViewerMotionFrame } from \"./file-viewer-motion-plan\";\nimport {\n  captureFileViewerFitWidthAnchorScreenOffset,\n  createFileViewerFitWidthSurfaceMotionResolver,\n  FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY,\n  getFileViewerFitWidthScale,\n  resolveFileViewerFitWidthMotionAnchorBlock,\n} from \"./file-viewer-fit-width-motion\";\nimport type { FileViewerDocumentSurfaceMotionResolver } from \"./file-viewer-motion-kernel\";\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 {\n  createImageFrameLayout,\n  getCurrentImageFrameNumber,\n  getImageFrameLayout,\n} from \"./image-viewer-virtualization\";\nimport { IMAGE_ZOOM_MOTION_TOTAL_MS } from \"./image-viewer-zoom-motion\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport function ImageViewerContent({\n  resource,\n  className,\n  scale: controlledScale,\n  defaultScale,\n  download = true,\n  onScaleChange,\n  controls = true,\n  renderFrameOverlay,\n  onFrameRenderTiming,\n  onVisibleFrameChange,\n  onScrollProgressChange,\n  bare = false,\n  forwardedRef,\n}: Omit<ImageViewerProps, \"source\"> & {\n  forwardedRef?: React.ForwardedRef<ImageViewerHandle>;\n  resource: ViewerResource;\n}) {\n  const frameSource = React.use(getImageSource(resource.content));\n  const sourceLeaseRef = useFrameSourceLease(resource.content, frameSource);\n  const rendererEnvironment = useOptionalFileViewerRendererEnvironment();\n  const { frameListRef, frameListWidth } = useFrameListWidth({\n    enabled: !rendererEnvironment.usesShellGeometry,\n  });\n  const rendererFrame = useOptionalFileViewerRendererFrame({\n    fallbackInlineSize: frameListWidth,\n  });\n  const frameListLayoutWidth = resolveFileViewerRendererLayoutInlineSize({\n    fallbackInlineSize: frameListWidth,\n    rendererFrame,\n  });\n  // While the sidebar transition is running, hold the virtualization's\n  // mounted-frame window so the reading frames stay mounted through the\n  // slide-start commit and the brief settle hold; re-derive the window\n  // normally once the transition is idle again.\n  const freezeVisibleFrameWindow = rendererFrame.phase !== \"idle\";\n  const {\n    isFitWidth,\n    rotateClockwise,\n    rotation,\n    scale,\n    scaleControlsDisabled,\n    setViewerScale,\n    widestFrameWidth,\n  } = useImageViewerScale(\n    frameSource,\n    controlledScale,\n    defaultScale,\n    onScaleChange,\n    frameListLayoutWidth,\n  );\n  const rasterInlineSize =\n    rendererFrame.shellInlineSize ??\n    rendererFrame.rasterInlineSize ??\n    frameListLayoutWidth;\n  const rasterScale = isFitWidth\n    ? clamp(\n        getFileViewerFitWidthScale({\n          availableInlineSize: rasterInlineSize ?? frameListLayoutWidth ?? 0,\n          contentInlineSize: widestFrameWidth,\n          stageInlinePadding: IMAGE_VIEWER_HORIZONTAL_PADDING,\n        }),\n        MIN_VIEWER_SCALE,\n        MAX_VIEWER_SCALE,\n      )\n    : scale;\n  const frameLayout = React.useMemo(\n    () =>\n      createImageFrameLayout({\n        frames: frameSource.frames,\n        scale,\n        rotation,\n      }),\n    [frameSource.frames, rotation, scale],\n  );\n  const {\n    captureZoomIntent,\n    currentFrameNumber,\n    getScrollMetrics,\n    getViewportElement,\n    handleScroll,\n    scrollToFrameArea,\n    setScrollViewportRef,\n  } = useVisibleFrame(\n    frameLayout,\n    frameSource,\n    onScrollProgressChange,\n    onVisibleFrameChange,\n  );\n  useImageViewerHandle(forwardedRef, getViewportElement, scrollToFrameArea);\n\n  // Toolbar zoom steps re-anchor the viewport center and relax a FLIP over\n  // the commit (image-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      IMAGE_ZOOM_MOTION_TOTAL_MS,\n    );\n    return () => clearTimeout(timeout);\n  });\n  const beginZoomMotion = React.useCallback(() => {\n    captureZoomIntent();\n    setZoomMotionSequence((sequence) => sequence + 1);\n  }, [captureZoomIntent]);\n\n  // Preserve the reading position when the image re-fits to a new width (the\n  // sidebar toggle). The fit scale is the layout key; capture the fraction on\n  // scroll and restore it the instant the scale changes. A toolbar zoom step\n  // must NOT take this path: its geometry commit already restored the\n  // viewport-CENTER anchor (image-viewer-zoom-motion), and the fraction\n  // restore would overwrite that scroll in the same commit. The sequence\n  // flips in the zoom gesture's own render, so the zoom's re-fit lands with\n  // the rebase disabled while the key still advances.\n  const scrollerRef = React.useRef<HTMLElement | null>(null);\n  const { captureReadingFraction } = useReadingFractionRebase({\n    scrollerRef,\n    layoutKey: scale,\n    enabled: rendererEnvironment.usesShellGeometry && !isZoomTransitioning,\n  });\n  const imageDocumentSurfaceRef = React.useRef<HTMLDivElement | null>(null);\n  const [imageDocumentSurfaceElement, setImageDocumentSurfaceElement] =\n    React.useState<HTMLDivElement | null>(null);\n  const resolveSurfaceMotionStyle =\n    React.useMemo<FileViewerDocumentSurfaceMotionResolver>(\n      () =>\n        createFileViewerFitWidthSurfaceMotionResolver({\n          // The frame centres the stage with auto margins whatever the\n          // renderer frame's align is (a zoomed-out document splits its\n          // leftover space evenly), so the margin model must say \"center\" too.\n          align: \"center\",\n          direction: rendererFrame.direction,\n          isFitWidth,\n          stageInlineSize: frameLayout.maxFrameWidth + frameLayout.padding * 2,\n          stageInlinePadding: IMAGE_VIEWER_HORIZONTAL_PADDING,\n          stageInlineSlope: 1,\n          stageBlockSlope: 1,\n        }),\n      [\n        frameLayout.maxFrameWidth,\n        frameLayout.padding,\n        isFitWidth,\n        rendererFrame.direction,\n      ],\n    );\n  const imageStageInlineSize =\n    frameLayout.maxFrameWidth + frameLayout.padding * 2;\n  const preMotionAnchorRef = React.useRef<{\n    frameNumber: number;\n    screenRelTop: number;\n  } | null>(null);\n  const lastAnchorBlockRef = React.useRef<number | null>(null);\n  const writeImageAnchorBlockOffsetPx = React.useCallback(\n    (anchorBlock: number) => {\n      const element = imageDocumentSurfaceRef.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 writeImageDocumentAnchorBlockOffset = 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    const physicalScrollTop = scrollerRef.current?.scrollTop ?? 0;\n    writeImageAnchorBlockOffsetPx(\n      Math.max(0, physicalScrollTop) +\n        Math.max(0, metrics.viewportHeight) * IMAGE_READING_MARKER_RATIO,\n    );\n  }, [getScrollMetrics, writeImageAnchorBlockOffsetPx]);\n  // The transform must pin the exact screen line the slide-start commit\n  // preserved. Measured against the frame layout models (old model at\n  // capture, new model at solve) in the stage's PHYSICAL coordinates — the\n  // frame offsets are logical, so they are mapped through the paged-scroll\n  // delta (logical − physical) like the PDF runtime does. Exact across\n  // constant gaps and padding, rebase clamps, paged scroll spaces, and\n  // mid-flight retargets (the capture applies the in-flight transform it was\n  // seen under).\n  const writeImageMotionAnchorBlockOffset = React.useCallback(() => {\n    const metrics = getScrollMetrics();\n    const physicalScrollTop = scrollerRef.current?.scrollTop ?? 0;\n    const logicalDelta = metrics.scrollTop - physicalScrollTop;\n    const preMotionAnchor = preMotionAnchorRef.current;\n    const newFrameLayout = preMotionAnchor\n      ? getImageFrameLayout(frameLayout, preMotionAnchor.frameNumber)\n      : null;\n    const anchorBlock =\n      preMotionAnchor && newFrameLayout\n        ? resolveFileViewerFitWidthMotionAnchorBlock({\n            fromInlineSize: rendererFrame.fromInlineSize,\n            probeScreenOffset: preMotionAnchor.screenRelTop,\n            probeStageOffset: newFrameLayout.offsetTop - logicalDelta,\n            scrollTop: physicalScrollTop,\n            stageInlineSize: imageStageInlineSize,\n            stageInlinePadding: IMAGE_VIEWER_HORIZONTAL_PADDING,\n            stageBlockSlope: 1,\n            toInlineSize: rendererFrame.toInlineSize,\n          })\n        : null;\n\n    if (anchorBlock == null) {\n      writeImageDocumentAnchorBlockOffset();\n      return;\n    }\n    writeImageAnchorBlockOffsetPx(anchorBlock);\n  }, [\n    frameLayout,\n    getScrollMetrics,\n    imageStageInlineSize,\n    rendererFrame.fromInlineSize,\n    rendererFrame.toInlineSize,\n    writeImageAnchorBlockOffsetPx,\n    writeImageDocumentAnchorBlockOffset,\n  ]);\n  const measureBeforeLayoutMotionRef = React.useRef(\n    (_liveFrame: FileViewerMotionFrame | null) => {},\n  );\n  measureBeforeLayoutMotionRef.current = (liveFrame) => {\n    const metrics = getScrollMetrics();\n    const physicalScrollTop = scrollerRef.current?.scrollTop ?? 0;\n    const logicalDelta = metrics.scrollTop - physicalScrollTop;\n    const frameNumber = getCurrentImageFrameNumber({\n      layout: frameLayout,\n      scrollTop: metrics.scrollTop,\n      viewportHeight: metrics.viewportHeight,\n    });\n    const frame = getImageFrameLayout(frameLayout, frameNumber);\n    preMotionAnchorRef.current = frame\n      ? {\n          frameNumber,\n          screenRelTop: captureFileViewerFitWidthAnchorScreenOffset({\n            lastAnchorBlock: lastAnchorBlockRef.current,\n            liveFrame,\n            probeStageOffset: frame.offsetTop - logicalDelta,\n            scrollTop: physicalScrollTop,\n            stageInlineSize: imageStageInlineSize,\n            stageInlinePadding: IMAGE_VIEWER_HORIZONTAL_PADDING,\n            stageBlockSlope: 1,\n          }),\n        }\n      : null;\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 isImageShellTransitioning = rendererFrame.isTransitioning;\n  const writeImageMotionAnchorBlockOffsetRef = React.useRef(\n    writeImageMotionAnchorBlockOffset,\n  );\n  writeImageMotionAnchorBlockOffsetRef.current =\n    writeImageMotionAnchorBlockOffset;\n  useKeyedLayoutEffect(\n    joinEffectKey([\n      \"image-motion-anchor\",\n      isImageShellTransitioning,\n      rendererFrame.documentTransition.transitionId,\n      writeImageMotionAnchorBlockOffset,\n    ]),\n    () => {\n      if (!isImageShellTransitioning) return;\n      writeImageMotionAnchorBlockOffsetRef.current();\n    },\n  );\n  const setImageDocumentSurfaceRef = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      const previousElement = imageDocumentSurfaceRef.current;\n      if (previousElement === element) return;\n      previousElement?.removeEventListener(\n        FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,\n        handleBeforeLayoutMotion,\n      );\n      imageDocumentSurfaceRef.current = element;\n      setImageDocumentSurfaceElement((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      writeImageDocumentAnchorBlockOffset();\n    },\n    [handleBeforeLayoutMotion, writeImageDocumentAnchorBlockOffset],\n  );\n  const motionProbeFrameNumberRef = React.useRef(currentFrameNumber);\n  motionProbeFrameNumberRef.current = currentFrameNumber;\n  const getImageMotionProbeElement = React.useCallback(() => {\n    const surface = imageDocumentSurfaceRef.current;\n    if (!surface) return null;\n    const frameNumber =\n      preMotionAnchorRef.current?.frameNumber ??\n      motionProbeFrameNumberRef.current;\n    return (\n      surface.querySelector<HTMLElement>(\n        `[data-slot=\"image-frame\"][data-frame-number=\"${frameNumber}\"]`,\n      ) ??\n      surface.querySelector<HTMLElement>(\n        '[data-slot=\"image-frame-slot\"][data-visible] [data-slot=\"image-frame\"]',\n      ) ??\n      surface.querySelector<HTMLElement>('[data-slot=\"image-frame\"]')\n    );\n  }, []);\n  const documentSurfaceKey = imageDocumentSurfaceElement\n    ? joinEffectKey([\n        \"image-document-surface\",\n        imageDocumentSurfaceElement,\n        rendererEnvironment.registerDocumentSurface,\n        resolveSurfaceMotionStyle,\n      ])\n    : null;\n  useKeyedLayoutEffect(documentSurfaceKey, () => {\n    if (!imageDocumentSurfaceElement) return;\n    return rendererEnvironment.registerDocumentSurface({\n      element: imageDocumentSurfaceElement,\n      getMotionProbeElement: getImageMotionProbeElement,\n      resolveMotionStyle: resolveSurfaceMotionStyle,\n    });\n  });\n  const setScrollViewportRefWithRebase = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      scrollerRef.current = element;\n      setScrollViewportRef(element);\n    },\n    [setScrollViewportRef],\n  );\n  const handleScrollWithRebase = React.useCallback(() => {\n    captureReadingFraction();\n    handleScroll();\n  }, [captureReadingFraction, handleScroll]);\n\n  const frameCount = frameSource.frames.length;\n  const countLabel =\n    frameSource.kind === \"tiff\"\n      ? `Page ${Math.min(currentFrameNumber, frameCount)} of ${frameCount}`\n      : `${frameCount} image${frameCount === 1 ? \"\" : \"s\"}`;\n  const zoomOut = React.useCallback(() => {\n    beginZoomMotion();\n    setViewerScale(clamp(scale / 1.2, MIN_VIEWER_SCALE, MAX_VIEWER_SCALE));\n  }, [beginZoomMotion, scale, setViewerScale]);\n  const zoomIn = React.useCallback(() => {\n    beginZoomMotion();\n    setViewerScale(clamp(scale * 1.2, MIN_VIEWER_SCALE, MAX_VIEWER_SCALE));\n  }, [beginZoomMotion, scale, setViewerScale]);\n  const fitWidth = React.useCallback(() => {\n    beginZoomMotion();\n    setViewerScale(null);\n  }, [beginZoomMotion, setViewerScale]);\n  useImageControlsRegistration({\n    countLabel,\n    download,\n    downloadAction: resource.originalDownload,\n    fitWidth,\n    rotateClockwise,\n    scale,\n    scaleControlsDisabled,\n    zoomIn,\n    zoomOut,\n  });\n\n  return (\n    <div\n      ref={sourceLeaseRef}\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=\"image-viewer\"\n    >\n      {controls ? (\n        <ViewerControls\n          position={{ label: countLabel }}\n          zoom={{\n            scale,\n            onZoomOut: zoomOut,\n            onZoomIn: zoomIn,\n            onFit: fitWidth,\n            isDisabled: scaleControlsDisabled,\n          }}\n          rotate={{ onRotate: rotateClockwise }}\n          downloads={download ? [resource.originalDownload] : []}\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            <ImageFrameScroller\n              source={frameSource}\n              layout={frameLayout}\n              scale={scale}\n              rotation={rotation}\n              rasterScale={rasterScale}\n              documentSurfaceRef={setImageDocumentSurfaceRef}\n              frameListRef={frameListRef}\n              getScrollMetrics={getScrollMetrics}\n              freezeVisibleFrameWindow={freezeVisibleFrameWindow}\n              isFitWidth={isFitWidth}\n              isZoomTransitioning={isZoomTransitioning}\n              viewportRef={setScrollViewportRefWithRebase}\n              onScroll={handleScrollWithRebase}\n              renderFrameOverlay={renderFrameOverlay}\n              onFrameRenderTiming={onFrameRenderTiming}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction useImageControlsRegistration({\n  countLabel,\n  download,\n  downloadAction,\n  fitWidth,\n  rotateClockwise,\n  scale,\n  scaleControlsDisabled,\n  zoomIn,\n  zoomOut,\n}: {\n  countLabel: string;\n  download: boolean;\n  downloadAction: ViewerResource[\"originalDownload\"];\n  fitWidth: () => void;\n  rotateClockwise: () => void;\n  scale: number;\n  scaleControlsDisabled: boolean;\n  zoomIn: () => void;\n  zoomOut: () => void;\n}) {\n  const onControlsChange = useViewerControlsRegistration();\n  const controlsState = React.useMemo<ViewerControlsState>(\n    () => ({\n      position: { label: countLabel },\n      zoom: {\n        scale,\n        onZoomOut: zoomOut,\n        onZoomIn: zoomIn,\n        onFit: fitWidth,\n        isDisabled: scaleControlsDisabled,\n      },\n      rotate: { onRotate: rotateClockwise },\n      downloads: download ? [downloadAction] : [],\n    }),\n    [\n      countLabel,\n      download,\n      downloadAction,\n      fitWidth,\n      rotateClockwise,\n      scale,\n      scaleControlsDisabled,\n      zoomIn,\n      zoomOut,\n    ],\n  );\n\n  useKeyedMountEffect(\n    joinEffectKey([\"image-controls\", onControlsChange, controlsState]),\n    () => {\n      if (!onControlsChange) return;\n      onControlsChange(controlsState);\n      return () => onControlsChange(null);\n    },\n  );\n}\n\nexport function getImageSource(\n  content: ImageSourceContent,\n): Promise<FrameSource> {\n  return imageFrameSourceManager.load(content, createTiffWorker);\n}\n\nexport function resetImageSourceCacheForTests() {\n  imageFrameSourceManager.clear();\n}\n\nfunction useFrameSourceLease(\n  content: ViewerContentIdentity,\n  source: FrameSource,\n): React.RefCallback<HTMLDivElement> {\n  return React.useCallback(\n    (element: HTMLDivElement | null) => {\n      if (!element) return;\n      const lease = retainImageSource(content, source);\n      return () => lease?.release();\n    },\n    [content, source],\n  );\n}\n\nfunction retainImageSource(\n  content: ViewerContentIdentity,\n  source: FrameSource,\n): FrameSourceLease | null {\n  return imageFrameSourceManager.retain(content, source);\n}\n\nfunction createTiffWorker() {\n  return new Worker(new URL(\"./image-viewer.worker\", import.meta.url), {\n    type: \"module\",\n  });\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/image-viewer-content.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer-hooks.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { type FrameSource } from \"@/lib/image-frame-source\";\nimport { normalizeRotation, rotatedSize } from \"@/lib/image-geometry\";\nimport {\n  type ImageViewerHandle,\n  type ImageViewerProps,\n} from \"@/components/ui/image-viewer-types\";\n\nimport {\n  getCurrentImageFrameNumber,\n  getImageFrameLayout,\n  getImageLogicalScrollTop,\n  getImagePhysicalScrollHeight,\n  resolveImagePhysicalScrollPosition,\n  type ImageFrameVirtualizationScrollMetrics,\n  type ImageFrameLayoutModel,\n} from \"./image-viewer-virtualization\";\nimport type {\n  ViewerDocumentLayoutModel,\n  ViewerDocumentScrollMapper,\n  ViewerDocumentScrollMetrics,\n  ViewerDocumentScrollTargetResolver,\n} from \"./viewer-types\";\nimport { useViewerDocumentScroll } from \"./viewer-document-scroll\";\nimport { createImageZoomMotionController } from \"./image-viewer-zoom-motion\";\nimport { getFileViewerFitWidthScale } from \"./file-viewer-fit-width-motion\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nconst IMAGE_SCROLL_HEADROOM = 48;\nexport const IMAGE_READING_MARKER_RATIO = 0.2;\nexport const IMAGE_VIEWER_HORIZONTAL_PADDING = 32;\n\n/** Bounds for the viewer's zoom range, shared by fit-width and the controls. */\nexport const MIN_VIEWER_SCALE = 0.1;\nexport const MAX_VIEWER_SCALE = 5;\n\nexport function useFrameListWidth({\n  enabled = true,\n}: {\n  enabled?: boolean;\n} = {}) {\n  const [frameListWidth, setFrameListWidth] = React.useState<number | null>(\n    null,\n  );\n  const frameListRef = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      if (!element) return;\n      if (!enabled) {\n        setFrameListWidth(null);\n        return;\n      }\n\n      setFrameListWidth(element.clientWidth);\n      const observer = new ResizeObserver((entries) => {\n        for (const entry of entries) {\n          setFrameListWidth((entry.target as HTMLElement).clientWidth);\n        }\n      });\n      observer.observe(element);\n      return () => observer.disconnect();\n    },\n    [enabled],\n  );\n\n  return { frameListRef, frameListWidth };\n}\n\nexport function useImageViewerScale(\n  source: FrameSource,\n  controlledScale: number | undefined,\n  defaultScale: number | undefined,\n  onScaleChange: ImageViewerProps[\"onScaleChange\"],\n  frameListWidth: number | null,\n) {\n  const isScaleControlled = controlledScale !== undefined;\n  const [uncontrolledScale, setUncontrolledScale] = React.useState<\n    number | null\n  >(() =>\n    defaultScale === undefined ? null : normalizeViewerScale(defaultScale),\n  );\n  const [rawRotation, setRawRotation] = React.useState(0);\n\n  useKeyedMountEffect(\n    joinEffectKey([\"image-scale-reset\", defaultScale, source]),\n    () => {\n      setRawRotation(0);\n      setUncontrolledScale(\n        defaultScale === undefined ? null : normalizeViewerScale(defaultScale),\n      );\n    },\n  );\n\n  const rotation = normalizeRotation(rawRotation);\n  const widestFrameWidth = Math.max(\n    1,\n    ...source.frames.map(\n      (frame) => rotatedSize(frame.intrinsicSize, rotation).width,\n    ),\n  );\n  const fitWidthScale = frameListWidth\n    ? getFileViewerFitWidthScale({\n        availableInlineSize: frameListWidth,\n        contentInlineSize: widestFrameWidth,\n        stageInlinePadding: IMAGE_VIEWER_HORIZONTAL_PADDING,\n      })\n    : 1;\n  const scale =\n    controlledScale !== undefined\n      ? normalizeViewerScale(controlledScale)\n      : uncontrolledScale !== null\n        ? normalizeViewerScale(uncontrolledScale)\n        : Math.min(MAX_VIEWER_SCALE, Math.max(MIN_VIEWER_SCALE, fitWidthScale));\n  const isFitWidth =\n    controlledScale === undefined && uncontrolledScale === null;\n\n  const scaleControlsDisabled = isScaleControlled && !onScaleChange;\n  const setViewerScale = React.useCallback(\n    (nextScale: number | null) => {\n      const normalized =\n        nextScale == null ? null : normalizeViewerScale(nextScale);\n      if (isScaleControlled) {\n        onScaleChange?.(normalized);\n        return;\n      }\n      setUncontrolledScale(normalized);\n    },\n    [isScaleControlled, onScaleChange],\n  );\n  const rotateClockwise = React.useCallback(() => {\n    setRawRotation((value) => (value + 90) % 360);\n  }, []);\n\n  return {\n    isFitWidth,\n    rotateClockwise,\n    rotation,\n    scale,\n    scaleControlsDisabled,\n    setViewerScale,\n    widestFrameWidth,\n  };\n}\n\nfunction normalizeViewerScale(scale: number): number {\n  return Number.isFinite(scale) && scale > 0\n    ? Math.min(MAX_VIEWER_SCALE, Math.max(MIN_VIEWER_SCALE, scale))\n    : MIN_VIEWER_SCALE;\n}\n\nfunction clamp01(value: number): number {\n  return Math.min(1, Math.max(0, value));\n}\n\nfunction normalizeFrameAreaPercent(value: number): number | null {\n  if (!Number.isFinite(value)) return null;\n  return Math.min(100, Math.max(0, value));\n}\n\ntype ImageReadingAnchor =\n  | {\n      kind: \"top\";\n    }\n  | {\n      frameNumber: number;\n      kind: \"frame\";\n      yPercent: number;\n    };\n\ntype ImageFrameArea = Parameters<ImageViewerHandle[\"scrollToFrameArea\"]>[1];\n\ntype ImageFrameAreaTarget = {\n  area: ImageFrameArea;\n  frameNumber: number;\n};\n\ntype ImageDocumentLayoutModel = ViewerDocumentLayoutModel<ImageReadingAnchor>;\n\nconst IMAGE_DOCUMENT_SCROLL_MAPPER: ViewerDocumentScrollMapper = {\n  getLogicalScrollTop: ({\n    blockSize,\n    physicalScrollTop,\n    scrollPageOffset,\n    viewportBlockSize,\n  }) =>\n    getImageLogicalScrollTop({\n      physicalScrollTop,\n      scrollPageOffset,\n      totalHeight: blockSize,\n      viewportHeight: viewportBlockSize,\n    }),\n  getPhysicalScrollSize: ({ blockSize, viewportBlockSize }) =>\n    getImagePhysicalScrollHeight({\n      totalHeight: blockSize,\n      viewportHeight: viewportBlockSize,\n    }),\n  resolvePhysicalScrollPosition: ({\n    blockSize,\n    logicalScrollTop,\n    scrollPageOffset,\n    viewportBlockSize,\n  }) =>\n    resolveImagePhysicalScrollPosition({\n      logicalScrollTop,\n      scrollPageOffset,\n      totalHeight: blockSize,\n      viewportHeight: viewportBlockSize,\n    }),\n};\n\nexport function useVisibleFrame(\n  layout: ImageFrameLayoutModel,\n  resetKey: unknown,\n  onScrollProgressChange: ImageViewerProps[\"onScrollProgressChange\"],\n  onVisibleFrameChange: ImageViewerProps[\"onVisibleFrameChange\"],\n) {\n  const [currentFrameNumber, setCurrentFrameNumber] = React.useState(1);\n  const lastReportedFrameNumber = React.useRef(0);\n  const documentLayout = React.useMemo(\n    () => createImageDocumentLayoutModel(layout),\n    [layout],\n  );\n  const resolveScrollTarget = React.useCallback<\n    ViewerDocumentScrollTargetResolver<ImageReadingAnchor, ImageFrameAreaTarget>\n  >(({ target }) => getImageFrameAreaScrollTarget(layout, target), [layout]);\n  const zoomMotion = React.useMemo(\n    () => createImageZoomMotionController(layout),\n    [layout],\n  );\n  const documentScroll = useViewerDocumentScroll({\n    copyScrollTarget: copyImageFrameAreaTarget,\n    layout: documentLayout,\n    resetKey,\n    resolveScrollTarget,\n    scrollMapper: IMAGE_DOCUMENT_SCROLL_MAPPER,\n    zoomMotion,\n  });\n  const getScrollMetrics = React.useCallback(\n    () =>\n      toImageFrameVirtualizationScrollMetrics(\n        documentScroll.getScrollMetrics(),\n      ),\n    [documentScroll],\n  );\n\n  const handleScroll = React.useCallback(() => {\n    const viewport = documentScroll.getViewportElement();\n    if (!viewport) return;\n    documentScroll.handleScroll();\n\n    const metrics = documentScroll.getScrollMetrics();\n    const isRebased = metrics.physicalScrollSize < documentLayout.blockSize;\n    const scrollable = isRebased\n      ? documentLayout.blockSize - metrics.viewportBlockSize\n      : viewport.scrollHeight - metrics.viewportBlockSize;\n    const progress =\n      scrollable > 0\n        ? clamp01(\n            (isRebased ? metrics.scrollTop : viewport.scrollTop) / scrollable,\n          )\n        : 0;\n    onScrollProgressChange?.(progress);\n\n    const frameNumber = getCurrentImageFrameNumber({\n      layout,\n      scrollTop: metrics.scrollTop,\n      viewportHeight: metrics.viewportBlockSize,\n    });\n    if (frameNumber && frameNumber !== lastReportedFrameNumber.current) {\n      lastReportedFrameNumber.current = frameNumber;\n      setCurrentFrameNumber(frameNumber);\n      onVisibleFrameChange?.(frameNumber);\n    }\n  }, [\n    documentLayout,\n    documentScroll,\n    layout,\n    onScrollProgressChange,\n    onVisibleFrameChange,\n  ]);\n\n  const scrollToFrameArea = React.useCallback(\n    (frameNumber: number, area: ImageFrameArea, options?: ScrollToOptions) => {\n      documentScroll.scrollToTarget({ area, frameNumber }, options);\n    },\n    [documentScroll],\n  );\n\n  useKeyedMountEffect(joinEffectKey([\"image-visible-reset\", resetKey]), () => {\n    lastReportedFrameNumber.current = 0;\n    setCurrentFrameNumber(1);\n  });\n\n  return {\n    captureZoomIntent: documentScroll.captureZoomIntent,\n    currentFrameNumber,\n    getScrollMetrics,\n    getViewportElement: documentScroll.getViewportElement,\n    handleScroll,\n    scrollToFrameArea,\n    scrollViewportElement: documentScroll.viewportElement,\n    setScrollViewportRef: documentScroll.setViewportElement,\n  };\n}\n\nfunction captureImageReadingAnchor(\n  layout: ImageFrameLayoutModel,\n  {\n    scrollTop,\n    viewportBlockSize,\n  }: {\n    scrollTop: number;\n    viewportBlockSize: number;\n  },\n): ImageReadingAnchor | null {\n  if (layout.frameCount === 0) return null;\n  if (scrollTop <= 0) return { kind: \"top\" };\n\n  const markerOffset =\n    scrollTop + viewportBlockSize * IMAGE_READING_MARKER_RATIO;\n  const frameNumber = getCurrentImageFrameNumber({\n    layout,\n    scrollTop,\n    viewportHeight: viewportBlockSize,\n  });\n  const frame = getImageFrameLayout(layout, frameNumber);\n  if (!frame || frame.height <= 0) return null;\n\n  return {\n    frameNumber,\n    kind: \"frame\",\n    yPercent: clamp01((markerOffset - frame.offsetTop) / frame.height),\n  };\n}\n\nfunction getImageReadingAnchorScrollTop(\n  layout: ImageFrameLayoutModel,\n  {\n    anchor,\n    viewportBlockSize,\n  }: {\n    anchor: ImageReadingAnchor;\n    viewportBlockSize: number;\n  },\n): number | null {\n  if (anchor.kind === \"top\") {\n    return 0;\n  }\n\n  const frame = getImageFrameLayout(layout, anchor.frameNumber);\n  if (!frame) return null;\n\n  const targetTop =\n    frame.offsetTop +\n    frame.height * anchor.yPercent -\n    viewportBlockSize * IMAGE_READING_MARKER_RATIO;\n  const maxScrollTop = Math.max(0, layout.totalHeight - viewportBlockSize);\n  return Math.min(maxScrollTop, Math.max(0, targetTop));\n}\n\nexport function useImageViewerHandle(\n  forwardedRef: React.ForwardedRef<ImageViewerHandle> | undefined,\n  getViewportElement: () => HTMLDivElement | null,\n  scrollToFrameArea: (\n    frameNumber: number,\n    area: ImageFrameArea,\n    options?: ScrollToOptions,\n  ) => void,\n) {\n  React.useImperativeHandle(\n    forwardedRef ?? null,\n    () => ({\n      scrollToFrameArea,\n      getViewportElement,\n    }),\n    [getViewportElement, scrollToFrameArea],\n  );\n}\n\nfunction createImageDocumentLayoutModel(\n  layout: ImageFrameLayoutModel,\n): ImageDocumentLayoutModel {\n  return {\n    blockSize: layout.totalHeight,\n    captureReadingAnchor: (input) => captureImageReadingAnchor(layout, input),\n    getReadingAnchorScrollTop: (target) =>\n      getImageReadingAnchorScrollTop(layout, target),\n    inlineSize: layout.maxFrameWidth,\n  };\n}\n\nfunction getImageFrameAreaScrollTarget(\n  layout: ImageFrameLayoutModel,\n  target: ImageFrameAreaTarget,\n) {\n  const areaTop = normalizeFrameAreaPercent(target.area.top);\n  if (areaTop == null) return null;\n\n  const frame = getImageFrameLayout(layout, target.frameNumber);\n  if (!frame) return null;\n\n  return {\n    top: Math.max(\n      0,\n      frame.offsetTop + (areaTop / 100) * frame.height - IMAGE_SCROLL_HEADROOM,\n    ),\n  };\n}\n\nfunction copyImageFrameAreaTarget(\n  target: ImageFrameAreaTarget,\n): ImageFrameAreaTarget {\n  return {\n    frameNumber: target.frameNumber,\n    area: {\n      top: target.area.top,\n      left: target.area.left,\n      width: target.area.width,\n      height: target.area.height,\n    },\n  };\n}\n\nfunction toImageFrameVirtualizationScrollMetrics(\n  metrics: ViewerDocumentScrollMetrics,\n): ImageFrameVirtualizationScrollMetrics {\n  return {\n    scrollPageOffset: metrics.scrollPageOffset,\n    scrollTop: metrics.scrollTop,\n    viewportHeight: metrics.viewportBlockSize,\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer-hooks.ts"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer-virtualization.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\nimport { type FrameDescriptor } from \"@/lib/image-frame-source\";\nimport {\n  frameCssSize,\n  frameIndexToNumber,\n  type QuarterTurn,\n} from \"@/lib/image-geometry\";\n\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport const IMAGE_FRAME_GAP = 16;\nexport const IMAGE_FRAME_PADDING = 16;\nexport const IMAGE_RENDER_FIT_PERFECTLY_OVERSCAN_PX =\n  IMAGE_FRAME_GAP + IMAGE_FRAME_PADDING;\nexport const IMAGE_RENDER_WINDOW_OVERSCAN_PX = 1000;\nexport const IMAGE_VISIBLE_FRAME_OVERSCAN = 2;\nexport const IMAGE_PRELOAD_FRAME_OVERSCAN = 4;\nexport const IMAGE_SCROLL_REBASE_CONTAINER_PX = 12_000_000;\nexport const IMAGE_SCROLL_REBASE_TRIGGER_PX = 1_000_000;\nexport const IMAGE_SCROLL_REBASE_TARGET_PX = 2_000_000;\nexport const IMAGE_SCROLL_REBASE_TARGET_BOTTOM_PX =\n  IMAGE_SCROLL_REBASE_CONTAINER_PX - IMAGE_SCROLL_REBASE_TARGET_PX;\nexport const IMAGE_SCROLL_REBASE_THRESHOLD_PX =\n  IMAGE_SCROLL_REBASE_CONTAINER_PX - IMAGE_SCROLL_REBASE_TRIGGER_PX;\n\nexport type ImageFrameLayout = {\n  frameIndex: number;\n  frameNumber: number;\n  width: number;\n  height: number;\n  offsetTop: number;\n};\n\nexport type ImageFrameLayoutModel = {\n  frameCount: number;\n  gap: number;\n  padding: number;\n  totalHeight: number;\n  maxFrameWidth: number;\n  frames: readonly ImageFrameLayout[];\n};\n\nexport type ImageRenderedFrameLayout = ImageFrameLayout & {\n  windowTop: number;\n};\n\nexport type ImageRenderedFrameWindow = {\n  afterHeight: number;\n  beforeHeight: number;\n  frames: readonly ImageRenderedFrameLayout[];\n  height: number;\n  stickyBottomInset: number;\n  stickyTopInset: number;\n};\n\nexport type ImageRenderPixelWindow = {\n  bottom: number;\n  top: number;\n};\n\nexport type ImageScrollRebasePosition = {\n  physicalScrollTop: number;\n  scrollPageOffset: number;\n};\n\nexport type ImageFrameVirtualizationScrollMetrics = {\n  scrollPageOffset: number;\n  scrollTop: number;\n  viewportHeight: number;\n};\n\ntype ImageFrameWindow = {\n  scrollPageOffset: number;\n  visibleFrameNumbers: readonly number[];\n  renderFrameNumbers: readonly number[];\n  preloadFrameNumbers: readonly number[];\n};\n\nexport function createImageFrameLayout({\n  frames,\n  scale,\n  rotation,\n  gap = IMAGE_FRAME_GAP,\n  padding = IMAGE_FRAME_PADDING,\n}: {\n  frames: readonly FrameDescriptor[];\n  scale: number;\n  rotation: QuarterTurn;\n  gap?: number;\n  padding?: number;\n}): ImageFrameLayoutModel {\n  // Inter-frame spacing scales with the image so gap / frame height stays\n  // constant through fit-width motion. The outer frame inset stays fixed on\n  // both axes: it is viewer chrome, and must exactly match the p-4 skeleton\n  // before and after the source loads.\n  const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1;\n  const blockGap = gap * safeScale;\n  const blockPadding = padding;\n  let offsetTop = blockPadding;\n  let maxFrameWidth = 0;\n  const frameLayouts = frames.map((frame, frameIndex) => {\n    const frameRect = frameCssSize(frame.intrinsicSize, scale, rotation);\n    const layout = {\n      frameIndex,\n      frameNumber: frameIndexToNumber(frameIndex),\n      width: frameRect.width,\n      height: frameRect.height,\n      offsetTop,\n    };\n    offsetTop += frameRect.height + blockGap;\n    maxFrameWidth = Math.max(maxFrameWidth, frameRect.width);\n    return layout;\n  });\n\n  return {\n    frameCount: frames.length,\n    gap: blockGap,\n    padding,\n    totalHeight: frames.length === 0 ? 0 : offsetTop - blockGap + blockPadding,\n    maxFrameWidth,\n    frames: frameLayouts,\n  };\n}\n\nexport function getImageFrameLayout(\n  layout: ImageFrameLayoutModel,\n  frameNumber: number,\n): ImageFrameLayout | undefined {\n  if (!Number.isInteger(frameNumber)) return undefined;\n  return layout.frames[frameNumber - 1];\n}\n\nexport function findImageFrameByOffset(\n  layout: ImageFrameLayoutModel,\n  offset: number,\n): number {\n  if (layout.frameCount === 0) return 1;\n\n  let low = 0;\n  let high = layout.frames.length - 1;\n  let match = 0;\n\n  while (low <= high) {\n    const mid = Math.floor((low + high) / 2);\n    if (layout.frames[mid].offsetTop <= offset) {\n      match = mid;\n      low = mid + 1;\n    } else {\n      high = mid - 1;\n    }\n  }\n\n  return layout.frames[match].frameNumber;\n}\n\nexport function getVisibleImageFrameNumbers({\n  layout,\n  scrollTop,\n  viewportHeight,\n  overscanFrames = IMAGE_VISIBLE_FRAME_OVERSCAN,\n}: {\n  layout: ImageFrameLayoutModel;\n  scrollTop: number;\n  viewportHeight: number;\n  overscanFrames?: number;\n}): readonly number[] {\n  if (layout.frameCount === 0) return [];\n\n  const measurementHeight = Math.max(1, viewportHeight);\n  return getImageFrameNumbersInRange({\n    layout,\n    startOffset: Math.max(0, scrollTop - measurementHeight),\n    endOffset: scrollTop + measurementHeight * 2,\n    overscanFrames,\n  });\n}\n\nexport function getImageRenderFrameNumbers({\n  fitPerfectly = false,\n  fitPerfectlyOverscanPx = IMAGE_RENDER_FIT_PERFECTLY_OVERSCAN_PX,\n  layout,\n  overscanPx = IMAGE_RENDER_WINDOW_OVERSCAN_PX,\n  scrollTop,\n  viewportHeight,\n}: {\n  fitPerfectly?: boolean;\n  fitPerfectlyOverscanPx?: number;\n  layout: ImageFrameLayoutModel;\n  overscanPx?: number;\n  scrollTop: number;\n  viewportHeight: number;\n}): readonly number[] {\n  if (layout.frameCount === 0) return [];\n\n  const window = createImageWindowFromScrollPosition({\n    fitPerfectly,\n    fitPerfectlyOverscanPx,\n    overscanPx,\n    scrollHeight: layout.totalHeight,\n    scrollTop,\n    viewportHeight,\n  });\n\n  return getImageFrameNumbersInRange({\n    layout,\n    startOffset: window.top,\n    endOffset: window.bottom,\n    overscanFrames: 0,\n  });\n}\n\nexport function getImagePreloadFrameNumbers({\n  layout,\n  renderFrameNumbers,\n  overscanFrames = IMAGE_PRELOAD_FRAME_OVERSCAN,\n}: {\n  layout: ImageFrameLayoutModel;\n  renderFrameNumbers: readonly number[];\n  overscanFrames?: number;\n}): readonly number[] {\n  if (layout.frameCount === 0 || renderFrameNumbers.length === 0) return [];\n\n  const firstRenderFrame = renderFrameNumbers[0];\n  const lastRenderFrame = renderFrameNumbers[renderFrameNumbers.length - 1];\n  return createFrameNumberRange(\n    Math.max(1, firstRenderFrame - overscanFrames),\n    Math.min(layout.frameCount, lastRenderFrame + overscanFrames),\n  );\n}\n\nexport function getImageRenderedFrameWindow({\n  frameNumbers,\n  layout,\n  physicalScrollHeight = layout.totalHeight,\n  scrollPageOffset = 0,\n  viewportHeight,\n}: {\n  frameNumbers: readonly number[];\n  layout: ImageFrameLayoutModel;\n  physicalScrollHeight?: number;\n  scrollPageOffset?: number;\n  viewportHeight: number;\n}): ImageRenderedFrameWindow | null {\n  const frames = frameNumbers\n    .map((frameNumber) => getImageFrameLayout(layout, frameNumber))\n    .filter((frame): frame is ImageFrameLayout => Boolean(frame))\n    .sort((left, right) => left.frameNumber - right.frameNumber);\n\n  if (frames.length === 0) return null;\n\n  const logicalBeforeHeight = frames[0].offsetTop;\n  const windowBottom = Math.max(\n    ...frames.map((frame) => frame.offsetTop + frame.height),\n  );\n  const height = Math.max(0, windowBottom - logicalBeforeHeight);\n  const beforeHeight = getImagePagedLayoutTop({\n    logicalTop: logicalBeforeHeight,\n    scrollPageOffset,\n    totalHeight: layout.totalHeight,\n    viewportHeight,\n  });\n  const safeViewportHeight =\n    Number.isFinite(viewportHeight) && viewportHeight > 0 ? viewportHeight : 0;\n  const safePhysicalScrollHeight = Math.max(\n    0,\n    finiteNumber(physicalScrollHeight),\n  );\n  const afterHeight = Math.max(\n    0,\n    safePhysicalScrollHeight - beforeHeight - height,\n  );\n  const stickyCoverInset =\n    safeViewportHeight > 0 ? -Math.max(0, height - safeViewportHeight) : 0;\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 frame, 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 includesFirstFrame = frames[0].frameNumber === 1;\n  const includesLastFrame =\n    frames[frames.length - 1].frameNumber === layout.frameCount;\n\n  return {\n    afterHeight,\n    beforeHeight,\n    frames: frames.map((frame) => ({\n      ...frame,\n      windowTop: frame.offsetTop - logicalBeforeHeight,\n    })),\n    height,\n    stickyBottomInset:\n      stickyCoverInset - (includesFirstFrame ? beforeHeight : 0),\n    stickyTopInset: stickyCoverInset - (includesLastFrame ? afterHeight : 0),\n  };\n}\n\nexport function createImageWindowFromScrollPosition({\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}): ImageRenderPixelWindow {\n  const safeOverscanPx = safePadding(overscanPx);\n  const safeScrollHeight = Math.max(0, finiteNumber(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 getImagePhysicalScrollHeight({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  const safeTotalHeight = safeSize(totalHeight);\n  return shouldRebaseImageScroll({\n    totalHeight: safeTotalHeight,\n    viewportHeight,\n  })\n    ? Math.min(safeTotalHeight, IMAGE_SCROLL_REBASE_CONTAINER_PX)\n    : safeTotalHeight;\n}\n\nexport function getImageLogicalScrollTop({\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    getImageMaxLogicalScrollTop({ totalHeight, viewportHeight }),\n  );\n}\n\nexport function resolveImagePhysicalScrollPosition({\n  logicalScrollTop,\n  scrollPageOffset,\n  totalHeight,\n  viewportHeight,\n}: {\n  logicalScrollTop: number;\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportHeight: number;\n}): ImageScrollRebasePosition {\n  const safeLogicalScrollTop = clamp(\n    finiteNumber(logicalScrollTop),\n    0,\n    getImageMaxLogicalScrollTop({ totalHeight, viewportHeight }),\n  );\n\n  if (!shouldRebaseImageScroll({ totalHeight, viewportHeight })) {\n    return {\n      physicalScrollTop: clamp(\n        safeLogicalScrollTop,\n        0,\n        getImageMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n      ),\n      scrollPageOffset: 0,\n    };\n  }\n\n  const currentPageOffset = clampImageScrollPageOffset({\n    scrollPageOffset,\n    totalHeight,\n    viewportHeight,\n  });\n  const physicalScrollTop = safeLogicalScrollTop - currentPageOffset;\n  const maxPhysicalScrollTop = getImageMaxPhysicalScrollTop({\n    totalHeight,\n    viewportHeight,\n  });\n  const maxPageOffset = getImageMaxScrollPageOffset({\n    totalHeight,\n    viewportHeight,\n  });\n  const shouldMoveDown =\n    physicalScrollTop > IMAGE_SCROLL_REBASE_THRESHOLD_PX &&\n    currentPageOffset < maxPageOffset;\n  const shouldMoveUp =\n    physicalScrollTop < IMAGE_SCROLL_REBASE_TRIGGER_PX && currentPageOffset > 0;\n\n  if (\n    physicalScrollTop < 0 ||\n    physicalScrollTop > maxPhysicalScrollTop ||\n    shouldMoveDown ||\n    shouldMoveUp\n  ) {\n    return resolveImageScrollPageWindow({\n      logicalScrollTop: safeLogicalScrollTop,\n      preferredPhysicalScrollTop: shouldMoveUp\n        ? Math.min(IMAGE_SCROLL_REBASE_TARGET_BOTTOM_PX, maxPhysicalScrollTop)\n        : IMAGE_SCROLL_REBASE_TARGET_PX,\n      totalHeight,\n      viewportHeight,\n    });\n  }\n\n  return {\n    physicalScrollTop: roundImageScrollPixel(\n      clamp(physicalScrollTop, 0, maxPhysicalScrollTop),\n    ),\n    scrollPageOffset: currentPageOffset,\n  };\n}\n\nexport function getImagePagedLayoutTop({\n  logicalTop,\n  scrollPageOffset,\n  totalHeight,\n  viewportHeight,\n}: {\n  logicalTop: number;\n  scrollPageOffset: number;\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  if (!shouldRebaseImageScroll({ totalHeight, viewportHeight })) {\n    return finiteNumber(logicalTop);\n  }\n  return Math.max(0, finiteNumber(logicalTop) - safePadding(scrollPageOffset));\n}\n\nexport function getCurrentImageFrameNumber({\n  layout,\n  scrollTop,\n  viewportHeight,\n}: {\n  layout: ImageFrameLayoutModel;\n  scrollTop: number;\n  viewportHeight: number;\n}): number {\n  return findImageFrameByOffset(layout, scrollTop + viewportHeight * 0.2);\n}\n\nexport function useImageFrameVirtualization({\n  freezeWindow = false,\n  getScrollMetrics,\n  layout,\n  resetKey,\n  sourceResetKey,\n  viewportElement,\n}: {\n  freezeWindow?: boolean;\n  getScrollMetrics?: () => ImageFrameVirtualizationScrollMetrics;\n  layout: ImageFrameLayoutModel;\n  resetKey?: unknown;\n  // Identity of the underlying source. `resetKey` changes on every re-fit\n  // (scale), but only a `sourceResetKey` change is a genuine new document that\n  // should snap the window back to the top; a re-fit preserves the reading\n  // position, so its window is re-derived from the live scroll instead.\n  sourceResetKey?: unknown;\n  viewportElement: HTMLDivElement | null;\n}) {\n  const resolvedSourceResetKey =\n    sourceResetKey === undefined ? resetKey : sourceResetKey;\n  const measureFrameRef = React.useRef(0);\n  const hasMeasuredScrollRef = React.useRef(false);\n  const lastMeasuredSourceResetKeyRef = React.useRef<unknown>(\n    resolvedSourceResetKey,\n  );\n  const lastMeasuredScrollTopRef = React.useRef(0);\n  const getCurrentScrollMetrics =\n    React.useCallback((): ImageFrameVirtualizationScrollMetrics => {\n      const sourceMatches = Object.is(\n        lastMeasuredSourceResetKeyRef.current,\n        resolvedSourceResetKey,\n      );\n      const metrics =\n        sourceMatches && getScrollMetrics\n          ? getScrollMetrics()\n          : {\n              scrollPageOffset: 0,\n              scrollTop: sourceMatches ? (viewportElement?.scrollTop ?? 0) : 0,\n              viewportHeight: viewportElement?.clientHeight ?? 0,\n            };\n      return reconcileImageScrollMetricsWithViewport(metrics, viewportElement);\n    }, [getScrollMetrics, resolvedSourceResetKey, viewportElement]);\n  const getFrameWindow = React.useCallback(\n    (\n      metrics: ImageFrameVirtualizationScrollMetrics,\n      fitPerfectly = false,\n    ): ImageFrameWindow => {\n      const renderFrameNumbers = getImageRenderFrameNumbers({\n        fitPerfectly,\n        layout,\n        scrollTop: metrics.scrollTop,\n        viewportHeight: metrics.viewportHeight,\n      });\n\n      return {\n        scrollPageOffset: metrics.scrollPageOffset,\n        visibleFrameNumbers: getVisibleImageFrameNumbers({\n          layout,\n          scrollTop: metrics.scrollTop,\n          viewportHeight: metrics.viewportHeight,\n        }),\n        renderFrameNumbers,\n        preloadFrameNumbers: getImagePreloadFrameNumbers({\n          layout,\n          renderFrameNumbers,\n        }),\n      };\n    },\n    [layout],\n  );\n  const getCurrentVisibleFrameNumbers =\n    React.useCallback((): ImageFrameWindow => {\n      return getFrameWindow(getCurrentScrollMetrics());\n    }, [getCurrentScrollMetrics, getFrameWindow]);\n  const getResetFrameWindow = React.useCallback(() => {\n    const viewportHeight = viewportElement?.clientHeight ?? 0;\n    const renderFrameNumbers = getImageRenderFrameNumbers({\n      layout,\n      scrollTop: 0,\n      viewportHeight,\n    });\n\n    return {\n      scrollPageOffset: 0,\n      visibleFrameNumbers: getVisibleImageFrameNumbers({\n        layout,\n        scrollTop: 0,\n        viewportHeight,\n      }),\n      renderFrameNumbers,\n      preloadFrameNumbers: getImagePreloadFrameNumbers({\n        layout,\n        renderFrameNumbers,\n      }),\n    };\n  }, [layout, viewportElement]);\n  const [state, setState] = React.useState<{\n    layout: ImageFrameLayoutModel;\n    resetKey: unknown;\n    sourceResetKey: unknown;\n    frameWindow: ImageFrameWindow;\n  }>(() => ({\n    layout,\n    resetKey,\n    sourceResetKey: resolvedSourceResetKey,\n    frameWindow: getCurrentVisibleFrameNumbers(),\n  }));\n  const frameWindow =\n    // While frozen (sidebar transition), keep the last committed window even\n    // when the re-fit changes the layout/resetKey, so the same reading frames\n    // stay mounted through the slide and settle rather than re-deriving from\n    // the transient top-of-document scroll position.\n    freezeWindow ||\n    (Object.is(state.layout, layout) && Object.is(state.resetKey, resetKey))\n      ? state.frameWindow\n      : // Snap to the top only for a genuine new document; a re-fit (same\n        // source, changed layout) preserves the reading position, so re-derive\n        // its window from the live scroll rather than resetting to the top.\n        Object.is(state.sourceResetKey, resolvedSourceResetKey)\n        ? getCurrentVisibleFrameNumbers()\n        : getResetFrameWindow();\n\n  const measureVisibleFramesNow = React.useCallback(() => {\n    measureFrameRef.current = 0;\n    // While the sidebar slide freezes the layout, the scroll container reports\n    // a transient top-of-document position, so re-deriving the visible-frame\n    // window here would window to the top and rebase the reading frame at\n    // settle. Hold the pre-slide window until the transition is idle again.\n    if (freezeWindow) return;\n    const metrics = getCurrentScrollMetrics();\n    const fitPerfectly = shouldFitImagePerfectly({\n      canFitPerfectly:\n        hasMeasuredScrollRef.current &&\n        Object.is(\n          lastMeasuredSourceResetKeyRef.current,\n          resolvedSourceResetKey,\n        ),\n      previousScrollTop: lastMeasuredScrollTopRef.current,\n      scrollTop: metrics.scrollTop,\n      viewportHeight: metrics.viewportHeight,\n    });\n    const nextFrameWindow = getFrameWindow(metrics, fitPerfectly);\n    lastMeasuredSourceResetKeyRef.current = resolvedSourceResetKey;\n    lastMeasuredScrollTopRef.current = metrics.scrollTop;\n    hasMeasuredScrollRef.current = true;\n    setState((previousState) =>\n      Object.is(previousState.layout, layout) &&\n      Object.is(previousState.resetKey, resetKey) &&\n      areFrameWindowsEqual(previousState.frameWindow, nextFrameWindow)\n        ? previousState\n        : {\n            layout,\n            resetKey,\n            sourceResetKey: resolvedSourceResetKey,\n            frameWindow: nextFrameWindow,\n          },\n    );\n    if (fitPerfectly && measureFrameRef.current === 0) {\n      measureFrameRef.current = requestAnimationFrame(() =>\n        measureVisibleFramesNowRef.current(),\n      );\n    }\n  }, [\n    freezeWindow,\n    getCurrentScrollMetrics,\n    getFrameWindow,\n    layout,\n    resetKey,\n    resolvedSourceResetKey,\n  ]);\n  const measureVisibleFramesNowRef = React.useRef(measureVisibleFramesNow);\n  useKeyedLayoutEffect(joinEffectKey([measureVisibleFramesNow]), () => {\n    measureVisibleFramesNowRef.current = measureVisibleFramesNow;\n  });\n\n  const measureVisibleFrames = React.useCallback(() => {\n    if (measureFrameRef.current) return;\n    measureFrameRef.current = requestAnimationFrame(() =>\n      measureVisibleFramesNowRef.current(),\n    );\n  }, []);\n\n  useKeyedMountEffect(\n    joinEffectKey([\"image-virtualization-measure\", measureVisibleFramesNow]),\n    () => {\n      if (measureFrameRef.current) {\n        cancelAnimationFrame(measureFrameRef.current);\n        measureFrameRef.current = 0;\n      }\n      measureVisibleFramesNow();\n    },\n  );\n\n  // When the sidebar transition finishes and the freeze lifts, the reading\n  // position has just been rebased by a settle scroll that may not have landed\n  // in the same commit as this idle edge. Schedule a deferred re-measure so the\n  // held window collapses back to the clean live window off the settled scroll;\n  // without this the post-cycle mounted-frame set can differ from the pre-cycle\n  // set (cycle-invariance \"changed\").\n  const wasFrozenRef = React.useRef(freezeWindow);\n  useKeyedLayoutEffect(joinEffectKey([\"image-freeze-edge\", freezeWindow]), () => {\n    const wasFrozen = wasFrozenRef.current;\n    wasFrozenRef.current = freezeWindow;\n    if (wasFrozen && !freezeWindow) {\n      measureVisibleFrames();\n    }\n  });\n\n  useMountEffect(() => {\n    return () => {\n      if (measureFrameRef.current) {\n        cancelAnimationFrame(measureFrameRef.current);\n      }\n    };\n  });\n\n  return {\n    scrollPageOffset: frameWindow.scrollPageOffset,\n    visibleFrameNumbers: frameWindow.visibleFrameNumbers,\n    renderFrameNumbers: frameWindow.renderFrameNumbers,\n    preloadFrameNumbers: frameWindow.preloadFrameNumbers,\n    measureVisibleFrames,\n  };\n}\n\n// On the cold path the document scroll model can momentarily report a\n// top-of-document scrollTop while the viewport is actually scrolled deep, which\n// would clobber the reading-frame window with the top window and then sit there\n// (a stable-but-wrong state the benchmark's stability wait accepts). When the\n// scroll is not page-rebased (no page offset) the logical scrollTop must equal\n// the live DOM scrollTop, so trust the viewport if the metrics disagree at top.\nfunction reconcileImageScrollMetricsWithViewport(\n  metrics: ImageFrameVirtualizationScrollMetrics,\n  viewportElement: HTMLDivElement | null,\n): ImageFrameVirtualizationScrollMetrics {\n  if (!viewportElement || metrics.scrollPageOffset !== 0) return metrics;\n  const domScrollTop = viewportElement.scrollTop;\n  if (domScrollTop > metrics.scrollTop + 1) {\n    return { ...metrics, scrollTop: domScrollTop };\n  }\n  return metrics;\n}\n\nfunction getImageFrameNumbersInRange({\n  layout,\n  startOffset,\n  endOffset,\n  overscanFrames,\n}: {\n  layout: ImageFrameLayoutModel;\n  startOffset: number;\n  endOffset: number;\n  overscanFrames: number;\n}) {\n  const safeStartOffset = Math.max(0, startOffset);\n  const safeEndOffset = Math.max(safeStartOffset, endOffset);\n  const firstVisibleFrame = findImageFrameByOffset(layout, safeStartOffset);\n  const lastVisibleFrame = findImageFrameByOffset(layout, safeEndOffset);\n  const firstFrame = Math.max(1, firstVisibleFrame - overscanFrames);\n  const lastFrame = Math.min(\n    layout.frameCount,\n    lastVisibleFrame + overscanFrames,\n  );\n\n  return createFrameNumberRange(firstFrame, lastFrame);\n}\n\nfunction createFrameNumberRange(firstFrame: number, lastFrame: number) {\n  if (lastFrame < firstFrame) return [];\n\n  return Array.from(\n    { length: lastFrame - firstFrame + 1 },\n    (_, index) => firstFrame + index,\n  );\n}\n\nfunction shouldRebaseImageScroll({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return (\n    getImageMaxLogicalScrollTop({ totalHeight, viewportHeight }) >\n    IMAGE_SCROLL_REBASE_THRESHOLD_PX\n  );\n}\n\nfunction getImageMaxLogicalScrollTop({\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 getImageMaxPhysicalScrollTop({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return Math.max(\n    getImagePhysicalScrollHeight({ totalHeight, viewportHeight }) -\n      Math.max(0, finiteNumber(viewportHeight)),\n    0,\n  );\n}\n\nfunction getImageMaxScrollPageOffset({\n  totalHeight,\n  viewportHeight,\n}: {\n  totalHeight: number;\n  viewportHeight: number;\n}) {\n  return Math.max(\n    getImageMaxLogicalScrollTop({ totalHeight, viewportHeight }) -\n      getImageMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n    0,\n  );\n}\n\nfunction clampImageScrollPageOffset({\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    getImageMaxScrollPageOffset({ totalHeight, viewportHeight }),\n  );\n}\n\nfunction resolveImageScrollPageWindow({\n  logicalScrollTop,\n  preferredPhysicalScrollTop,\n  totalHeight,\n  viewportHeight,\n}: {\n  logicalScrollTop: number;\n  preferredPhysicalScrollTop: number;\n  totalHeight: number;\n  viewportHeight: number;\n}): ImageScrollRebasePosition {\n  let physicalScrollTop = roundImageScrollPixel(\n    clamp(\n      finiteNumber(preferredPhysicalScrollTop),\n      0,\n      getImageMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n    ),\n  );\n  let scrollPageOffset = clampImageScrollPageOffset({\n    scrollPageOffset: logicalScrollTop - physicalScrollTop,\n    totalHeight,\n    viewportHeight,\n  });\n\n  physicalScrollTop = roundImageScrollPixel(\n    clamp(\n      logicalScrollTop - scrollPageOffset,\n      0,\n      getImageMaxPhysicalScrollTop({ totalHeight, viewportHeight }),\n    ),\n  );\n  scrollPageOffset = clampImageScrollPageOffset({\n    scrollPageOffset: logicalScrollTop - physicalScrollTop,\n    totalHeight,\n    viewportHeight,\n  });\n\n  return { physicalScrollTop, scrollPageOffset };\n}\n\nfunction shouldFitImagePerfectly({\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 + IMAGE_RENDER_WINDOW_OVERSCAN_PX * 2\n  );\n}\n\nfunction areFrameWindowsEqual(\n  previousFrameWindow: ImageFrameWindow,\n  nextFrameWindow: ImageFrameWindow,\n): boolean {\n  return (\n    previousFrameWindow.scrollPageOffset === nextFrameWindow.scrollPageOffset &&\n    areFrameNumbersEqual(\n      previousFrameWindow.visibleFrameNumbers,\n      nextFrameWindow.visibleFrameNumbers,\n    ) &&\n    areFrameNumbersEqual(\n      previousFrameWindow.renderFrameNumbers,\n      nextFrameWindow.renderFrameNumbers,\n    ) &&\n    areFrameNumbersEqual(\n      previousFrameWindow.preloadFrameNumbers,\n      nextFrameWindow.preloadFrameNumbers,\n    )\n  );\n}\n\nfunction finiteNumber(value: number) {\n  return Number.isFinite(value) ? value : 0;\n}\n\nfunction roundImageScrollPixel(value: number) {\n  if (typeof window === \"undefined\") return Math.round(value);\n  const ratio = window.devicePixelRatio || 1;\n  return Math.round(value * ratio) / ratio;\n}\n\nfunction safePadding(value: number) {\n  return Number.isFinite(value) && value > 0 ? value : 0;\n}\n\nfunction safeSize(value: number) {\n  return Number.isFinite(value) && value > 0 ? value : 0;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n\nfunction areFrameNumbersEqual(\n  previousFrameNumbers: readonly number[],\n  nextFrameNumbers: readonly number[],\n): boolean {\n  if (previousFrameNumbers.length !== nextFrameNumbers.length) return false;\n  return previousFrameNumbers.every(\n    (frameNumber, index) => frameNumber === nextFrameNumbers[index],\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer-virtualization.ts"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer-zoom-motion.ts",
      "content": "import {\n  findImageFrameByOffset,\n  getImageFrameLayout,\n  type ImageFrameLayoutModel,\n} from \"./image-viewer-virtualization\";\nimport type { ViewerDocumentZoomMotionController } from \"./viewer-types\";\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 IMAGE_ZOOM_CENTER_MARKER_RATIO = 0.5;\nconst IMAGE_ZOOM_MOTION_DURATION_MS = 200;\n// easeOutCubic: fast attack, gentle landing — a linear relax reads as a\n// hard stop at settle.\nconst IMAGE_ZOOM_MOTION_EASING = \"cubic-bezier(0.33, 1, 0.68, 1)\";\nconst IMAGE_ZOOM_MOTION_CLEANUP_MS = IMAGE_ZOOM_MOTION_DURATION_MS + 50;\nconst IMAGE_ZOOM_MOTION_MIN_TRANSLATE_PX = 0.5;\nconst IMAGE_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 frame 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 as an un-animated 620px snap on a multi-frame TIFF's fit-width. 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 IMAGE_ZOOM_MOTION_AXIS_FROZEN_DELTA = 0.02;\nconst IMAGE_ZOOM_MOTION_AXIS_MOVED_DELTA = 0.05;\nconst IMAGE_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 IMAGE_ZOOM_MOTION_TOTAL_MS = IMAGE_ZOOM_MOTION_CLEANUP_MS + 50;\n\nconst IMAGE_ZOOM_SCROLL_RANGE_SELECTOR =\n  '[data-slot=\"image-viewer-scroll-range\"]';\nconst IMAGE_ZOOM_VISUAL_LAYER_SELECTOR =\n  '[data-slot=\"image-viewer-visual-clip\"]';\n\nexport type ImageZoomTransaction = {\n  frameNumber: number;\n  /**\n   * Deliberately unclamped, like the reading anchor: the layout is linear in\n   * scale, so a center marker resting in a gap or the edge padding restores\n   * by the same frame-relative 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 stage inside the pane.\n   * The model path below stays the fallback for a rebased (paged) scroll,\n   * where the stage stops spanning the document.\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 createImageZoomMotionController(\n  layout: ImageFrameLayoutModel,\n): ViewerDocumentZoomMotionController<ImageZoomTransaction> {\n  return {\n    capture: ({ scrollTop, viewportElement }) =>\n      captureImageZoomTransaction({ layout, scrollTop, viewportElement }),\n    resolveScrollTarget: ({ transaction, viewportElement }) =>\n      resolveImageZoomScrollTarget({ layout, transaction, viewportElement }),\n    play: ({ transaction, viewportElement }) =>\n      playImageZoomMotion({ transaction, viewportElement }),\n  };\n}\n\nexport function captureImageZoomTransaction({\n  layout,\n  scrollTop,\n  viewportElement,\n}: {\n  layout: ImageFrameLayoutModel;\n  scrollTop: number;\n  viewportElement: HTMLDivElement;\n}): ImageZoomTransaction | null {\n  if (layout.frameCount === 0) return null;\n\n  const viewportBlockSize = Math.max(0, viewportElement.clientHeight);\n  const centerOffset =\n    Math.max(0, scrollTop) + viewportBlockSize * IMAGE_ZOOM_CENTER_MARKER_RATIO;\n  const frameNumber = findImageFrameByOffset(layout, centerOffset);\n  const frameLayout = getImageFrameLayout(layout, frameNumber);\n  if (!frameLayout || frameLayout.height <= 0) return null;\n\n  return {\n    frameNumber,\n    yPercent: (centerOffset - frameLayout.offsetTop) / frameLayout.height,\n    inlineFraction: captureImageZoomInlineFraction(viewportElement),\n    blockFraction: captureImageZoomBlockFraction(viewportElement),\n    previousVisualRect: readElementRect(\n      findImageZoomVisualLayer(viewportElement),\n    ),\n  };\n}\n\nexport function resolveImageZoomScrollTarget({\n  layout,\n  transaction,\n  viewportElement,\n}: {\n  layout: ImageFrameLayoutModel;\n  transaction: ImageZoomTransaction;\n  viewportElement: HTMLDivElement;\n}): { left?: number; top: number } | null {\n  const frameLayout = getImageFrameLayout(layout, transaction.frameNumber);\n  if (!frameLayout) return null;\n\n  const viewportBlockSize = Math.max(0, viewportElement.clientHeight);\n  const maxScrollTop = Math.max(0, layout.totalHeight - viewportBlockSize);\n  const top = clamp(\n    resolveImageZoomScrollTop({ layout, transaction, viewportElement }) ??\n      frameLayout.offsetTop +\n        frameLayout.height * transaction.yPercent -\n        viewportBlockSize * IMAGE_ZOOM_CENTER_MARKER_RATIO,\n    0,\n    maxScrollTop,\n  );\n\n  const left = resolveImageZoomScrollLeft(viewportElement, transaction);\n  return { top, ...(left == null ? null : { left }) };\n}\n\n// The stage (scroll range) is measured by live rects rather than re-deriving\n// its centered offset, so the math is direction-agnostic: RTL scrollLeft\n// coordinate spaces and the browser's own clamping both fall out for free.\nfunction captureImageZoomInlineFraction(viewportElement: HTMLDivElement) {\n  const rangeRect = readElementRect(\n    viewportElement.querySelector<HTMLElement>(\n      IMAGE_ZOOM_SCROLL_RANGE_SELECTOR,\n    ),\n  );\n  if (!rangeRect || rangeRect.width <= 0) return null;\n  return (\n    (getViewportCenterX(viewportElement) - rangeRect.left) / rangeRect.width\n  );\n}\n\nfunction captureImageZoomBlockFraction(viewportElement: HTMLDivElement) {\n  const rangeRect = readElementRect(\n    viewportElement.querySelector<HTMLElement>(\n      IMAGE_ZOOM_SCROLL_RANGE_SELECTOR,\n    ),\n  );\n  if (!rangeRect || rangeRect.height <= 0) return null;\n  return (\n    (getViewportCenterY(viewportElement) - rangeRect.top) / rangeRect.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 document; a rebased scroll detaches the two, and the\n// caller falls back to the frame model.\nfunction resolveImageZoomScrollTop({\n  layout,\n  transaction,\n  viewportElement,\n}: {\n  layout: ImageFrameLayoutModel;\n  transaction: ImageZoomTransaction;\n  viewportElement: HTMLDivElement;\n}) {\n  if (transaction.blockFraction == null) return null;\n  const rangeRect = readElementRect(\n    viewportElement.querySelector<HTMLElement>(\n      IMAGE_ZOOM_SCROLL_RANGE_SELECTOR,\n    ),\n  );\n  if (!rangeRect || rangeRect.height <= 0) return null;\n  if (Math.abs(rangeRect.height - layout.totalHeight) > 2) return null;\n  return (\n    viewportElement.scrollTop +\n    (rangeRect.top + rangeRect.height * transaction.blockFraction) -\n    getViewportCenterY(viewportElement)\n  );\n}\n\nfunction resolveImageZoomScrollLeft(\n  viewportElement: HTMLDivElement,\n  transaction: ImageZoomTransaction,\n) {\n  if (transaction.inlineFraction == null) return undefined;\n  const rangeRect = readElementRect(\n    viewportElement.querySelector<HTMLElement>(\n      IMAGE_ZOOM_SCROLL_RANGE_SELECTOR,\n    ),\n  );\n  if (!rangeRect || rangeRect.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    (rangeRect.left + rangeRect.width * transaction.inlineFraction) -\n    getViewportCenterX(viewportElement)\n  );\n}\n\nexport function playImageZoomMotion({\n  transaction,\n  viewportElement,\n}: {\n  transaction: ImageZoomTransaction;\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 = findImageZoomVisualLayer(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 (hasDetachedImageZoomAxes(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) > IMAGE_ZOOM_MOTION_MIN_TRANSLATE_PX ||\n    Math.abs(translateY) > IMAGE_ZOOM_MOTION_MIN_TRANSLATE_PX ||\n    Math.abs(1 - scaleX) > IMAGE_ZOOM_MOTION_MIN_SCALE_DELTA ||\n    Math.abs(1 - scaleY) > IMAGE_ZOOM_MOTION_MIN_SCALE_DELTA;\n  if (!hasVisibleDelta) return null;\n\n  // Re-express the FLIP about the viewport center instead of the stage's\n  // top-left: the stage is the full document (potentially hundreds of\n  // thousands of px tall), and a scale that far from its origin runs into\n  // GPU float precision. Anchoring at the viewport keeps the rasterized\n  // region's 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    visualLayer.style.transition = \"\";\n    visualLayer.style.transform = \"\";\n    visualLayer.style.transformOrigin = \"\";\n    visualLayer.style.willChange = \"\";\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 ${IMAGE_ZOOM_MOTION_DURATION_MS}ms ${IMAGE_ZOOM_MOTION_EASING}`;\n    visualLayer.style.transform = \"translate3d(0px, 0px, 0px) scale(1, 1)\";\n  });\n  cleanupTimeout = setTimeout(finish, IMAGE_ZOOM_MOTION_CLEANUP_MS);\n\n  return finish;\n}\n\nfunction findImageZoomVisualLayer(viewportElement: HTMLDivElement) {\n  if (typeof viewportElement.querySelector !== \"function\") return null;\n  return viewportElement.querySelector<HTMLElement>(\n    IMAGE_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 hasDetachedImageZoomAxes(scaleX: number, scaleY: number) {\n  const inlineDelta = Math.abs(scaleX - 1);\n  const blockDelta = Math.abs(scaleY - 1);\n  const frozenAxis =\n    (blockDelta < IMAGE_ZOOM_MOTION_AXIS_FROZEN_DELTA &&\n      inlineDelta > IMAGE_ZOOM_MOTION_AXIS_MOVED_DELTA) ||\n    (inlineDelta < IMAGE_ZOOM_MOTION_AXIS_FROZEN_DELTA &&\n      blockDelta > IMAGE_ZOOM_MOTION_AXIS_MOVED_DELTA);\n  return (\n    frozenAxis ||\n    Math.abs(scaleX - scaleY) >\n      IMAGE_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\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(max, Math.max(min, value));\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer-zoom-motion.ts"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer-frame.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\nimport {\n  ImageSourceDisposedError,\n  toImageFormatError,\n  type FrameSource,\n} from \"@/lib/image-frame-source\";\nimport {\n  frameCssSize,\n  frameIndexToNumber,\n  type FrameOverlayProps,\n  type QuarterTurn,\n} from \"@/lib/image-geometry\";\nimport { isResourceError } from \"@/lib/viewer-errors\";\nimport {\n  type ImageFrameRenderTiming,\n  type ImageViewerProps,\n} from \"@/components/ui/image-viewer-types\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\n\nimport {\n  getImageFrameLayout,\n  getImagePhysicalScrollHeight,\n  getImageRenderedFrameWindow,\n  useImageFrameVirtualization,\n  type ImageFrameLayout,\n  type ImageFrameLayoutModel,\n  type ImageFrameVirtualizationScrollMetrics,\n  type ImageRenderedFrameLayout,\n  type ImageRenderedFrameWindow,\n} from \"./image-viewer-virtualization\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nconst IMAGE_SCROLL_IDLE_MS = 120;\nconst IMAGE_RETAINED_DECODED_PIXEL_BUDGET = 24_000_000;\nconst IMAGE_RETAINED_RENDERED_PIXEL_BUDGET = 32_000_000;\nconst IMAGE_FRAME_RENDER_CONCURRENCY = 4;\nconst IMAGE_LOW_PRIORITY_FRAME_RENDER_CONCURRENCY = 1;\n\ntype ImageRenderQuality = \"high\" | \"low\";\n\nexport interface ImageFrameProps {\n  source: FrameSource;\n  frameIndex: number;\n  scale: number;\n  rasterScale?: number;\n  rotation: QuarterTurn;\n  renderQuality?: ImageRenderQuality;\n  renderOverlay?: (props: FrameOverlayProps) => React.ReactNode;\n  onFrameRenderTiming?: (timing: ImageFrameRenderTiming) => void;\n}\n\ntype ImageProjectionCallback = NonNullable<\n  | ImageViewerProps[\"onFrameRenderTiming\"]\n  | ImageViewerProps[\"renderFrameOverlay\"]\n>;\n\nexport interface ImageFrameScrollerProps {\n  source: FrameSource;\n  layout: ImageFrameLayoutModel;\n  scale: number;\n  rasterScale?: number;\n  rotation: QuarterTurn;\n  documentSurfaceRef?: React.Ref<HTMLDivElement>;\n  frameListRef: React.Ref<HTMLDivElement>;\n  getScrollMetrics?: () => ImageFrameVirtualizationScrollMetrics;\n  freezeVisibleFrameWindow?: boolean;\n  /** The frames are at their fit-width scale (the shell motion's domain). */\n  isFitWidth?: boolean;\n  /** A toolbar zoom step's FLIP relax is in flight (image-viewer-zoom-motion). */\n  isZoomTransitioning?: boolean;\n  viewportRef: React.Ref<HTMLDivElement>;\n  onScroll: () => void;\n  renderFrameOverlay?: ImageViewerProps[\"renderFrameOverlay\"];\n  onFrameRenderTiming?: ImageViewerProps[\"onFrameRenderTiming\"];\n}\n\nexport function ImageFrame({\n  source,\n  frameIndex,\n  scale,\n  rasterScale = scale,\n  rotation,\n  renderQuality = \"high\",\n  renderOverlay,\n  onFrameRenderTiming,\n}: ImageFrameProps) {\n  const descriptor = source.frames[frameIndex];\n  const frameRect = frameCssSize(descriptor.intrinsicSize, scale, rotation);\n  const frameNumber = frameIndexToNumber(frameIndex);\n\n  return (\n    <div\n      className=\"ring-border relative shadow-sm ring-1\"\n      style={{ width: frameRect.width, height: frameRect.height }}\n      data-slot=\"image-frame\"\n      data-frame={frameNumber}\n      data-frame-number={frameNumber}\n    >\n      <ImageFrameCanvas\n        source={source}\n        frameIndex={frameIndex}\n        layoutScale={scale}\n        rasterScale={rasterScale}\n        rotation={rotation}\n        renderQuality={renderQuality}\n        onFrameRenderTiming={onFrameRenderTiming}\n      />\n      {renderOverlay ? (\n        <div className=\"pointer-events-none absolute inset-0\">\n          {renderOverlay({\n            frameNumber,\n            frameRect,\n            scale,\n            rotation,\n          })}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nfunction ImageFrameCanvas({\n  source,\n  frameIndex,\n  layoutScale,\n  rasterScale,\n  rotation,\n  renderQuality,\n  onFrameRenderTiming,\n}: {\n  source: FrameSource;\n  frameIndex: number;\n  layoutScale: number;\n  rasterScale: number;\n  rotation: QuarterTurn;\n  renderQuality: ImageRenderQuality;\n  onFrameRenderTiming?: (timing: ImageFrameRenderTiming) => void;\n}) {\n  const descriptor = source.frames[frameIndex];\n  const dpr = getImageDevicePixelRatio();\n  const layoutFrameRect = frameCssSize(\n    descriptor.intrinsicSize,\n    layoutScale,\n    rotation,\n  );\n  // One resample generation: the canvas backing never exceeds the bitmap's\n  // intrinsic resolution. Rastering past intrinsic only launders the same\n  // pixels through a second resample, and the compositor's in-flight rescale\n  // then beats against that pre-resampled texture (visible stroke shimmer).\n  const rasterDeviceScale = Math.min(rasterScale * dpr, 1);\n  const backingFrameRect = frameCssSize(\n    descriptor.intrinsicSize,\n    rasterDeviceScale,\n    rotation,\n  );\n  const [drawError, setDrawError] = React.useState<Error | null>(null);\n\n  if (drawError) throw drawError;\n\n  const canvasRef = React.useCallback(\n    (canvas: HTMLCanvasElement | null) => {\n      if (!canvas) return;\n      const ctx = canvas.getContext(\"2d\");\n      if (!ctx) return;\n\n      const canvasWidth = Math.max(1, Math.floor(backingFrameRect.width));\n      const canvasHeight = Math.max(1, Math.floor(backingFrameRect.height));\n      const cached = source.hasDecodedFrame(frameIndex);\n      const previousCanvas = resizeImageCanvas(\n        canvas,\n        canvasWidth,\n        canvasHeight,\n        { preserve: !cached },\n      );\n      if (previousCanvas) {\n        preserveCanvasImage(ctx, previousCanvas, canvasWidth, canvasHeight);\n      }\n\n      let cancelled = false;\n      let settled = false;\n      let reported = false;\n      const startedAt = now();\n      const renderScale = rasterDeviceScale;\n      const report = (status: ImageFrameRenderTiming[\"status\"]) => {\n        if (reported) return;\n        reported = true;\n        notifyImageFrameRenderTiming(onFrameRenderTiming, {\n          cached,\n          durationMs: now() - startedAt,\n          frameNumber: frameIndexToNumber(frameIndex),\n          pixelRatio: dpr,\n          renderScale,\n          status,\n        });\n      };\n      source\n        .acquire(frameIndex)\n        .then((bitmap) => {\n          settled = true;\n          if (cancelled) return;\n          ctx.save();\n          try {\n            ctx.translate(\n              backingFrameRect.width / 2,\n              backingFrameRect.height / 2,\n            );\n            ctx.rotate((rotation * Math.PI) / 180);\n            const drawWidth =\n              descriptor.intrinsicSize.width * rasterDeviceScale;\n            const drawHeight =\n              descriptor.intrinsicSize.height * rasterDeviceScale;\n            ctx.imageSmoothingQuality = renderQuality;\n            ctx.drawImage(\n              bitmap,\n              -drawWidth / 2,\n              -drawHeight / 2,\n              drawWidth,\n              drawHeight,\n            );\n            canvas.dataset.imageFrameRendered = \"true\";\n          } finally {\n            ctx.restore();\n          }\n          report(\"rendered\");\n        })\n        .catch((error) => {\n          settled = true;\n          if (error instanceof ImageSourceDisposedError) return;\n          if (!cancelled) {\n            report(\"failed\");\n            setDrawError(\n              isResourceError(error)\n                ? error\n                : toImageFormatError(error, {\n                    kind: \"decode_failed\",\n                    message: \"Image decode failed\",\n                  }),\n            );\n          }\n        });\n\n      return () => {\n        cancelled = true;\n        if (!settled) report(\"cancelled\");\n        source.release(frameIndex);\n      };\n    },\n    [\n      backingFrameRect.height,\n      backingFrameRect.width,\n      descriptor.intrinsicSize.height,\n      descriptor.intrinsicSize.width,\n      frameIndex,\n      onFrameRenderTiming,\n      rasterDeviceScale,\n      renderQuality,\n      rotation,\n      source,\n    ],\n  );\n\n  return (\n    <canvas\n      ref={canvasRef}\n      style={{ width: layoutFrameRect.width, height: layoutFrameRect.height }}\n      className=\"block bg-white\"\n    />\n  );\n}\n\nexport function ImageFrameScroller({\n  source,\n  layout,\n  scale,\n  rasterScale = scale,\n  rotation,\n  documentSurfaceRef,\n  frameListRef,\n  getScrollMetrics,\n  freezeVisibleFrameWindow = false,\n  isFitWidth = true,\n  isZoomTransitioning = false,\n  viewportRef,\n  onScroll,\n  renderFrameOverlay,\n  onFrameRenderTiming,\n}: ImageFrameScrollerProps) {\n  const canvasRef = React.useRef<HTMLDivElement | null>(null);\n  const projectionFrameRef = React.useRef<number | null>(null);\n  const idleTimerRef = React.useRef<number | null>(null);\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null);\n  const [viewportElement, setViewportElement] =\n    React.useState<HTMLDivElement | null>(null);\n  const projectionCacheRef = React.useRef<ImageFrameProjectionCache>({\n    clock: 0,\n    frames: new Map(),\n    resetKey: \"\",\n  });\n  const scrollStateRef = React.useRef({\n    isScrolling: false,\n  });\n  const sourceKey = getImageProjectionSourceKey(source);\n  const layoutResetKey = [\n    sourceKey,\n    layout.frameCount,\n    layout.maxFrameWidth,\n    layout.totalHeight,\n    scale,\n    rotation,\n  ].join(\"\\u0000\");\n  const projectionResetKey = String(sourceKey);\n  const {\n    scrollPageOffset,\n    visibleFrameNumbers,\n    renderFrameNumbers,\n    preloadFrameNumbers,\n    measureVisibleFrames,\n  } = useImageFrameVirtualization({\n    freezeWindow: freezeVisibleFrameWindow,\n    getScrollMetrics,\n    layout,\n    resetKey: layoutResetKey,\n    sourceResetKey: projectionResetKey,\n    viewportElement,\n  });\n  const viewportHeight = viewportElement?.clientHeight ?? 0;\n  const physicalScrollHeight = getImagePhysicalScrollHeight({\n    totalHeight: layout.totalHeight,\n    viewportHeight,\n  });\n  const documentInlineSize = layout.maxFrameWidth + layout.padding * 2;\n  const renderedWindow = React.useMemo(\n    () =>\n      getImageRenderedFrameWindow({\n        frameNumbers: renderFrameNumbers,\n        layout,\n        physicalScrollHeight,\n        scrollPageOffset,\n        viewportHeight,\n      }),\n    [\n      layout,\n      physicalScrollHeight,\n      renderFrameNumbers,\n      scrollPageOffset,\n      viewportHeight,\n    ],\n  );\n  const imageDevicePixelRatio = getImageDevicePixelRatio();\n  const lowPriorityFrameNumbers = React.useMemo(\n    () => [...renderFrameNumbers, ...preloadFrameNumbers],\n    [preloadFrameNumbers, renderFrameNumbers],\n  );\n  const {\n    activeFrameNumbers,\n    onFrameRenderTiming: handleScheduledFrameRenderTiming,\n  } = useImageFrameRenderScheduler({\n    frameNumbers: visibleFrameNumbers,\n    lowPriorityFrameNumbers,\n    rasterScale,\n    rotation,\n    pixelRatio: imageDevicePixelRatio,\n    resetKey: layoutResetKey,\n    maxRunning: IMAGE_FRAME_RENDER_CONCURRENCY,\n    maxLowPriorityRunning: IMAGE_LOW_PRIORITY_FRAME_RENDER_CONCURRENCY,\n  });\n  const handleFrameRenderTiming = React.useCallback(\n    (timing: ImageFrameRenderTiming) => {\n      handleScheduledFrameRenderTiming(timing);\n      onFrameRenderTiming?.(timing);\n    },\n    [handleScheduledFrameRenderTiming, onFrameRenderTiming],\n  );\n\n  const projectFrames = React.useCallback(() => {\n    projectionFrameRef.current = null;\n    projectImageFrames({\n      activeFrameNumbers,\n      cache: projectionCacheRef.current,\n      canvas: canvasRef.current,\n      layout,\n      onFrameRenderTiming: handleFrameRenderTiming,\n      preloadFrameNumbers,\n      renderFrameOverlay,\n      renderQuality: scrollStateRef.current.isScrolling ? \"low\" : \"high\",\n      renderedWindow,\n      renderFrameNumbers,\n      resetKey: projectionResetKey,\n      rotation,\n      rasterScale,\n      scale,\n      source,\n      sourceKey,\n      visibleFrameNumbers,\n    });\n  }, [\n    activeFrameNumbers,\n    handleFrameRenderTiming,\n    layout,\n    preloadFrameNumbers,\n    projectionResetKey,\n    renderFrameOverlay,\n    renderedWindow,\n    renderFrameNumbers,\n    rotation,\n    rasterScale,\n    scale,\n    freezeVisibleFrameWindow,\n    source,\n    sourceKey,\n    visibleFrameNumbers,\n  ]);\n  const projectFramesRef = React.useRef(projectFrames);\n  useKeyedLayoutEffect(joinEffectKey([projectFrames]), () => {\n    projectFramesRef.current = projectFrames;\n  });\n\n  const scheduleProjectFrames = React.useCallback(() => {\n    if (projectionFrameRef.current !== null) return;\n    if (typeof requestAnimationFrame !== \"function\") {\n      projectFramesRef.current();\n      return;\n    }\n    projectionFrameRef.current = requestAnimationFrame(() => {\n      projectFramesRef.current();\n    });\n  }, []);\n\n  useKeyedMountEffect(joinEffectKey([\"image-project\", projectFrames]), () => {\n    projectFrames();\n  });\n\n  // Commit-then-relax: a re-fit commit (scale/layout/rotation change) must\n  // project the frame boxes BEFORE paint — the shell motion transform is\n  // computed for the settled layout, so stale boxes under it would paint one\n  // mismatched frame. Scroll-driven window updates keep the deferred (rAF)\n  // projection above.\n  useKeyedLayoutEffect(\n    joinEffectKey([\"image-project-refit\", layoutResetKey]),\n    () => {\n      projectFramesRef.current();\n    },\n  );\n\n  useMountEffect(() => {\n    return () => {\n      if (\n        projectionFrameRef.current !== null &&\n        typeof cancelAnimationFrame === \"function\"\n      ) {\n        cancelAnimationFrame(projectionFrameRef.current);\n      }\n      if (idleTimerRef.current !== null) {\n        window.clearTimeout(idleTimerRef.current);\n      }\n      disposeImageFrameProjectionCache(projectionCacheRef.current);\n    };\n  });\n\n  const setViewportRef = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      viewportElementRef.current = element;\n      setViewportElement(element);\n      assignImageRef(viewportRef, element);\n    },\n    [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    measureVisibleFrames();\n    scrollStateRef.current.isScrolling = true;\n\n    if (idleTimerRef.current !== null) {\n      window.clearTimeout(idleTimerRef.current);\n    }\n    idleTimerRef.current = window.setTimeout(() => {\n      idleTimerRef.current = null;\n      scrollStateRef.current.isScrolling = false;\n      scheduleProjectFrames();\n    }, IMAGE_SCROLL_IDLE_MS);\n  }, [measureVisibleFrames, onScroll, scheduleProjectFrames]);\n\n  return (\n    <ScrollArea\n      className=\"min-h-0 flex-1\"\n      viewportRef={setViewportRef}\n      viewportProps={{\n        onScroll: handleScroll,\n        style: { overflowAnchor: \"none\" },\n      }}\n    >\n      {/* The outer clip exists for ONE state: a fit-width shell slide, where\n          the kernel's counter-transform paints the stage past its committed\n          box, and that visual overflow would otherwise inflate the scroller's\n          scrollable area and drag a max-clamped scroll position mid-flight.\n          Every other state must NOT clip: a zoomed-in stage's inline overflow\n          IS the horizontal scroll range (an unconditional clip froze\n          scrollWidth at the viewport width and made zoomed images\n          horizontally unscrollable), and a zoom relax's enlarged opening\n          frame must not be cut at the committed box. At fit-width the stage\n          fits the layout width, so the active clip can never eat scrollable\n          overflow. */}\n      <div\n        ref={frameListRef}\n        className={`relative flex min-h-full w-full flex-col ${\n          freezeVisibleFrameWindow && isFitWidth\n            ? \"overflow-clip\"\n            : \"overflow-visible\"\n        }`}\n        data-slot=\"image-viewer-fit-width-measure\"\n      >\n        {/* The stage is a CAMERA view: a zoom step shrinks it about the\n            viewport centre, so leftover space splits evenly. Auto margins do\n            that and collapse to 0 once the stage overflows, keeping the scroll\n            origin (and every pixel) reachable — and the fit-width motion\n            resolver is handed the matching \"center\" margin model, since a\n            mismatch there leaves the auto-margin re-centering uncompensated\n            mid-slide. The BLOCK axis centres only outside fit-width: at\n            fit-width a pane resize re-fits the frames, and half of that height\n            delta is motion the shell transform does not model (measured as a\n            4.4px hop). Zoomed, that transform is identity and the height is\n            pane-independent, so the centring is inert during a slide. The flex\n            column exists only to give those margins free space to split. */}\n        <div\n          data-slot=\"image-viewer-scroll-range\"\n          className={`relative mx-auto shrink-0 ${isFitWidth ? \"\" : \"my-auto\"}`}\n          style={{\n            contain: \"layout size style\",\n            height: physicalScrollHeight,\n            minWidth: documentInlineSize,\n            width: documentInlineSize,\n          }}\n        >\n          {/* Shell motion counter-transforms the document surface; a zoom\n              step relaxes a FLIP on the visual clip itself. Either way the\n              in-flight paint is larger than the committed box on at least one\n              leg, so the clip must let go for the duration. Identity fits the\n              clip again at settle, so restoring it cannot move a pixel. */}\n          <div\n            data-slot=\"image-viewer-visual-clip\"\n            className=\"absolute inset-0\"\n            style={{\n              contain:\n                freezeVisibleFrameWindow || isZoomTransitioning\n                  ? \"style\"\n                  : \"paint style\",\n              overflow:\n                freezeVisibleFrameWindow || isZoomTransitioning\n                  ? \"visible\"\n                  : \"clip\",\n            }}\n          >\n            <div\n              ref={documentSurfaceRef}\n              className=\"relative\"\n              data-slot=\"image-viewer-document\"\n              style={{\n                contain: \"layout style\",\n                height: physicalScrollHeight,\n                minWidth: documentInlineSize,\n                width: documentInlineSize,\n              }}\n            >\n              {renderedWindow ? (\n                <>\n                  <div\n                    aria-hidden\n                    data-slot=\"image-frame-window-before\"\n                    style={{\n                      contain: \"layout size\",\n                      height: renderedWindow.beforeHeight,\n                    }}\n                  />\n                  <div\n                    className=\"sticky\"\n                    data-slot=\"image-frame-sticky-window\"\n                    style={{\n                      bottom: renderedWindow.stickyBottomInset,\n                      contain: \"layout style inline-size\",\n                      height: renderedWindow.height,\n                      isolation: \"isolate\",\n                      top: renderedWindow.stickyTopInset,\n                    }}\n                  >\n                    <div\n                      ref={setCanvasRef}\n                      className=\"relative mx-auto h-full w-full\"\n                      data-slot=\"image-frame-virtual-canvas\"\n                      style={{\n                        contain: \"layout style\",\n                        height: renderedWindow.height,\n                        minWidth: documentInlineSize,\n                        width: documentInlineSize,\n                      }}\n                    />\n                  </div>\n                  <div\n                    aria-hidden\n                    data-slot=\"image-frame-window-after\"\n                    style={{\n                      contain: \"layout size\",\n                      height: renderedWindow.afterHeight,\n                    }}\n                  />\n                </>\n              ) : null}\n            </div>\n          </div>\n        </div>\n      </div>\n    </ScrollArea>\n  );\n}\n\ntype ImageFrameProjectionCache = {\n  clock: number;\n  frames: Map<number, ProjectedImageFrame>;\n  resetKey: string;\n};\n\ntype ProjectedImageFrame = {\n  decodedPixels: number;\n  isLive: boolean;\n  lastSeen: number;\n  renderedPixels: number;\n  renderKey: string;\n  root: Root;\n  shell: HTMLElement;\n};\n\nconst imageProjectionSourceKeys = new WeakMap<FrameSource, number>();\nconst imageProjectionCallbackKeys = new WeakMap<\n  ImageProjectionCallback,\n  number\n>();\nlet nextImageProjectionSourceKey = 1;\nlet nextImageProjectionCallbackKey = 1;\n\ntype ImageFrameRenderRequest = {\n  frameNumber: number;\n  pixelRatio: number;\n  renderScale: number;\n  rotation: QuarterTurn;\n};\n\nfunction useImageFrameRenderScheduler({\n  frameNumbers,\n  lowPriorityFrameNumbers = [],\n  rasterScale,\n  rotation,\n  pixelRatio,\n  resetKey,\n  maxRunning = IMAGE_FRAME_RENDER_CONCURRENCY,\n  maxLowPriorityRunning = IMAGE_LOW_PRIORITY_FRAME_RENDER_CONCURRENCY,\n}: {\n  frameNumbers: readonly number[];\n  lowPriorityFrameNumbers?: readonly number[];\n  rasterScale: number;\n  rotation: QuarterTurn;\n  pixelRatio: number;\n  resetKey: unknown;\n  maxRunning?: number;\n  maxLowPriorityRunning?: number;\n}) {\n  const renderScale = Math.min(rasterScale * pixelRatio, 1);\n  const requestedRenders = React.useMemo<ImageFrameRenderRequest[]>(\n    () =>\n      mergeImageFrameNumbers(frameNumbers).map((frameNumber) => ({\n        frameNumber,\n        pixelRatio,\n        renderScale,\n        rotation,\n      })),\n    [frameNumbers, pixelRatio, renderScale, rotation],\n  );\n  const lowPriorityRequestedRenders = React.useMemo<\n    ImageFrameRenderRequest[]\n  >(() => {\n    const primaryFrameNumberSet = new Set(frameNumbers);\n    return mergeImageFrameNumbers(lowPriorityFrameNumbers)\n      .filter((frameNumber) => !primaryFrameNumberSet.has(frameNumber))\n      .map((frameNumber) => ({\n        frameNumber,\n        pixelRatio,\n        renderScale,\n        rotation,\n      }));\n  }, [\n    frameNumbers,\n    lowPriorityFrameNumbers,\n    pixelRatio,\n    renderScale,\n    rotation,\n  ]);\n  const allRequestedRenders = React.useMemo(\n    () => [...requestedRenders, ...lowPriorityRequestedRenders],\n    [lowPriorityRequestedRenders, requestedRenders],\n  );\n  const requestedRendersRef = React.useRef(allRequestedRenders);\n  const resetKeyRef = React.useRef(resetKey);\n  const renderContextRef = React.useRef({ rotation });\n  useKeyedLayoutEffect(\n    joinEffectKey([allRequestedRenders, resetKey, rotation]),\n    () => {\n      requestedRendersRef.current = allRequestedRenders;\n      resetKeyRef.current = resetKey;\n      renderContextRef.current = { rotation };\n    },\n  );\n\n  const [state, setState] = React.useState<{\n    renderedByKey: ReadonlyMap<string, ImageFrameRenderRequest>;\n    resetKey: unknown;\n  }>(() => ({ renderedByKey: new Map(), resetKey }));\n  const emptyRenderedByKey = React.useMemo<\n    ReadonlyMap<string, ImageFrameRenderRequest>\n  >(() => new Map(), []);\n  const renderedByKey = Object.is(state.resetKey, resetKey)\n    ? state.renderedByKey\n    : emptyRenderedByKey;\n\n  useKeyedMountEffect(joinEffectKey([allRequestedRenders, resetKey]), () => {\n    setState((previousState) => {\n      if (!Object.is(previousState.resetKey, resetKey)) {\n        return { renderedByKey: new Map(), resetKey };\n      }\n\n      const renderedByKey = new Map<string, ImageFrameRenderRequest>();\n      for (const [key, rendered] of previousState.renderedByKey) {\n        if (\n          allRequestedRenders.some((request) =>\n            doesRenderedImageFrameSatisfyRequest(rendered, request),\n          )\n        ) {\n          renderedByKey.set(key, rendered);\n        }\n      }\n\n      return areImageRenderMapsEqual(previousState.renderedByKey, renderedByKey)\n        ? previousState\n        : { renderedByKey, resetKey };\n    });\n  });\n\n  const activeFrameNumbers = React.useMemo(() => {\n    const renderedFrameNumbers: number[] = [];\n    const pendingFrameNumbers: number[] = [];\n    const lowPriorityRenderedFrameNumbers: number[] = [];\n    const lowPriorityPendingFrameNumbers: number[] = [];\n\n    for (const request of requestedRenders) {\n      if (isImageRenderRequestSatisfied(request, renderedByKey)) {\n        renderedFrameNumbers.push(request.frameNumber);\n      } else {\n        pendingFrameNumbers.push(request.frameNumber);\n      }\n    }\n    for (const request of lowPriorityRequestedRenders) {\n      if (isImageRenderRequestSatisfied(request, renderedByKey)) {\n        lowPriorityRenderedFrameNumbers.push(request.frameNumber);\n      } else {\n        lowPriorityPendingFrameNumbers.push(request.frameNumber);\n      }\n    }\n\n    const activePrimaryFrameNumbers = [\n      ...renderedFrameNumbers,\n      ...pendingFrameNumbers.slice(0, Math.max(1, maxRunning)),\n    ];\n    const activeLowPriorityFrameNumbers =\n      pendingFrameNumbers.length > 0\n        ? lowPriorityRenderedFrameNumbers\n        : [\n            ...lowPriorityRenderedFrameNumbers,\n            ...lowPriorityPendingFrameNumbers.slice(\n              0,\n              Math.max(0, maxLowPriorityRunning),\n            ),\n          ];\n\n    return mergeImageFrameNumbers([\n      ...activePrimaryFrameNumbers,\n      ...activeLowPriorityFrameNumbers,\n    ]);\n  }, [\n    lowPriorityRequestedRenders,\n    maxLowPriorityRunning,\n    maxRunning,\n    renderedByKey,\n    requestedRenders,\n  ]);\n\n  const onFrameRenderTiming = React.useCallback(\n    (timing: ImageFrameRenderTiming) => {\n      const rendered: ImageFrameRenderRequest = {\n        frameNumber: timing.frameNumber,\n        pixelRatio: timing.pixelRatio,\n        renderScale: timing.renderScale,\n        rotation: renderContextRef.current.rotation,\n      };\n      const key = getImageFrameRenderKey(rendered);\n\n      setState((previousState) => {\n        const resetKey = resetKeyRef.current;\n        const previousRenderedByKey = Object.is(\n          previousState.resetKey,\n          resetKey,\n        )\n          ? previousState.renderedByKey\n          : new Map<string, ImageFrameRenderRequest>();\n        const nextRenderedByKey = new Map(previousRenderedByKey);\n\n        if (\n          timing.status === \"rendered\" &&\n          requestedRendersRef.current.some((request) =>\n            doesRenderedImageFrameSatisfyRequest(rendered, request),\n          )\n        ) {\n          nextRenderedByKey.set(key, rendered);\n        } else {\n          nextRenderedByKey.delete(key);\n        }\n\n        return areImageRenderMapsEqual(previousRenderedByKey, nextRenderedByKey)\n          ? previousState\n          : { renderedByKey: nextRenderedByKey, resetKey };\n      });\n    },\n    [],\n  );\n\n  return { activeFrameNumbers, onFrameRenderTiming };\n}\n\nfunction mergeImageFrameNumbers(frameNumbers: readonly number[]) {\n  return [...new Set(frameNumbers)].sort((left, right) => left - right);\n}\n\nfunction getImageFrameRenderKey({\n  frameNumber,\n  pixelRatio,\n  renderScale,\n  rotation,\n}: ImageFrameRenderRequest) {\n  return `${frameNumber}:${renderScale}:${rotation}:${pixelRatio}`;\n}\n\nfunction isImageRenderRequestSatisfied(\n  request: ImageFrameRenderRequest,\n  renderedByKey: ReadonlyMap<string, ImageFrameRenderRequest>,\n) {\n  for (const rendered of renderedByKey.values()) {\n    if (doesRenderedImageFrameSatisfyRequest(rendered, request)) return true;\n  }\n  return false;\n}\n\nfunction doesRenderedImageFrameSatisfyRequest(\n  rendered: ImageFrameRenderRequest,\n  request: ImageFrameRenderRequest,\n) {\n  return (\n    rendered.frameNumber === request.frameNumber &&\n    rendered.renderScale === request.renderScale &&\n    rendered.rotation === request.rotation &&\n    rendered.pixelRatio >= request.pixelRatio\n  );\n}\n\nfunction areImageRenderMapsEqual(\n  left: ReadonlyMap<string, ImageFrameRenderRequest>,\n  right: ReadonlyMap<string, ImageFrameRenderRequest>,\n) {\n  if (left.size !== right.size) return false;\n  for (const [key, value] of left) {\n    if (right.get(key) !== value) return false;\n  }\n  return true;\n}\n\nfunction projectImageFrames({\n  activeFrameNumbers,\n  cache,\n  canvas,\n  layout,\n  onFrameRenderTiming,\n  preloadFrameNumbers,\n  renderFrameOverlay,\n  renderQuality,\n  renderedWindow,\n  renderFrameNumbers,\n  resetKey,\n  rotation,\n  rasterScale,\n  scale,\n  source,\n  sourceKey,\n  visibleFrameNumbers,\n}: {\n  activeFrameNumbers: readonly number[];\n  cache: ImageFrameProjectionCache;\n  canvas: HTMLDivElement | null;\n  layout: ImageFrameLayoutModel;\n  onFrameRenderTiming?: ImageViewerProps[\"onFrameRenderTiming\"];\n  preloadFrameNumbers: readonly number[];\n  renderFrameOverlay?: ImageViewerProps[\"renderFrameOverlay\"];\n  renderQuality: ImageRenderQuality;\n  renderedWindow: ImageRenderedFrameWindow | null;\n  renderFrameNumbers: readonly number[];\n  resetKey: string;\n  rotation: QuarterTurn;\n  rasterScale: number;\n  scale: number;\n  source: FrameSource;\n  sourceKey: number;\n  visibleFrameNumbers: readonly number[];\n}) {\n  if (cache.resetKey !== resetKey) {\n    disposeImageFrameProjectionCache(cache);\n    cache.resetKey = resetKey;\n  }\n  if (!canvas || !renderedWindow) return;\n\n  cache.clock += 1;\n  const activeFrameNumberSet = new Set(activeFrameNumbers);\n  const renderedFrameNumberSet = new Set(renderFrameNumbers);\n  const visibleFrameNumberSet = new Set(visibleFrameNumbers);\n  const activePreloadFrameNumbers = activeFrameNumbers.filter(\n    (frameNumber) => !renderedFrameNumberSet.has(frameNumber),\n  );\n  const retainedFrameIndexes = new Set<number>();\n\n  let previousShell: HTMLElement | null = null;\n  for (const frame of renderedWindow.frames) {\n    retainedFrameIndexes.add(frame.frameIndex);\n    const existingFrame = cache.frames.get(frame.frameIndex);\n    if (!existingFrame && !activeFrameNumberSet.has(frame.frameNumber))\n      continue;\n    const projectedFrame = existingFrame ?? createProjectedImageFrame(frame);\n    projectedFrame.lastSeen = cache.clock;\n    projectedFrame.decodedPixels = frameDecodedPixels(source, frame.frameIndex);\n    projectedFrame.renderedPixels = frameRenderedPixels(frame);\n    patchProjectedImageFrame({\n      frame,\n      isVisible: visibleFrameNumberSet.has(frame.frameNumber),\n      shell: projectedFrame.shell,\n    });\n    if (activeFrameNumberSet.has(frame.frameNumber)) {\n      // The raster is prepared for the widest shell layout before motion.\n      // Sidebar toggles then change only its CSS box and the shell transform:\n      // the backing store never resizes (or sharpens) inside the flight.\n      renderProjectedImageFrame({\n        frame,\n        onFrameRenderTiming,\n        projectedFrame,\n        renderFrameOverlay,\n        renderQuality,\n        rasterScale,\n        rotation,\n        scale,\n        source,\n        sourceKey,\n      });\n    }\n    cache.frames.set(frame.frameIndex, projectedFrame);\n    placeProjectedImageFrame(canvas, projectedFrame.shell, previousShell);\n    previousShell = projectedFrame.shell;\n  }\n\n  detachUnrenderedImageFrames(cache, retainedFrameIndexes);\n  schedulePrefetchImageFrames({\n    frameNumbers: activePreloadFrameNumbers,\n    layout,\n    source,\n  });\n  evictImageFrameProjectionCache(cache, retainedFrameIndexes);\n}\n\nfunction createProjectedImageFrame(\n  frame: ImageRenderedFrameLayout,\n): ProjectedImageFrame {\n  const shell = document.createElement(\"div\");\n  shell.className = \"absolute top-0 left-1/2\";\n  shell.dataset.slot = \"image-frame-slot\";\n  shell.dataset.virtualFrameNumber = String(frame.frameNumber);\n\n  return {\n    decodedPixels: 0,\n    isLive: true,\n    lastSeen: 0,\n    renderedPixels: 0,\n    renderKey: \"\",\n    root: createRoot(shell),\n    shell,\n  };\n}\n\nfunction patchProjectedImageFrame({\n  frame,\n  isVisible,\n  shell,\n}: {\n  frame: ImageRenderedFrameLayout;\n  isVisible: boolean;\n  shell: HTMLElement;\n}) {\n  shell.dataset.virtualFrameNumber = String(frame.frameNumber);\n  if (isVisible) {\n    shell.dataset.visible = \"\";\n  } else {\n    delete shell.dataset.visible;\n  }\n  setImageStyle(shell, \"transform\", `translate(-50%, ${frame.windowTop}px)`);\n  setImagePixelStyle(shell, \"width\", frame.width);\n  setImagePixelStyle(shell, \"height\", frame.height);\n}\n\nfunction renderProjectedImageFrame({\n  frame,\n  onFrameRenderTiming,\n  projectedFrame,\n  renderFrameOverlay,\n  renderQuality,\n  rasterScale,\n  rotation,\n  scale,\n  source,\n  sourceKey,\n}: {\n  frame: ImageRenderedFrameLayout;\n  onFrameRenderTiming?: ImageViewerProps[\"onFrameRenderTiming\"];\n  projectedFrame: ProjectedImageFrame;\n  renderFrameOverlay?: ImageViewerProps[\"renderFrameOverlay\"];\n  renderQuality: ImageRenderQuality;\n  rasterScale: number;\n  rotation: QuarterTurn;\n  scale: number;\n  source: FrameSource;\n  sourceKey: number;\n}) {\n  const renderKey = [\n    frame.frameIndex,\n    sourceKey,\n    frame.width,\n    frame.height,\n    scale,\n    rasterScale,\n    rotation,\n    renderQuality,\n    getImageProjectionCallbackKey(renderFrameOverlay),\n    getImageProjectionCallbackKey(onFrameRenderTiming),\n    getImageDevicePixelRatio(),\n  ].join(\"\\u0000\");\n\n  if (projectedFrame.renderKey === renderKey) return;\n  projectedFrame.renderKey = renderKey;\n  projectedFrame.root.render(\n    <ImageFrame\n      source={source}\n      frameIndex={frame.frameIndex}\n      scale={scale}\n      rasterScale={rasterScale}\n      rotation={rotation}\n      renderQuality={renderQuality}\n      onFrameRenderTiming={onFrameRenderTiming}\n      renderOverlay={\n        renderFrameOverlay\n          ? ({ frameNumber, frameRect, scale, rotation }) =>\n              renderFrameOverlay({\n                frameNumber,\n                width: frameRect.width,\n                height: frameRect.height,\n                scale,\n                rotation,\n              })\n          : undefined\n      }\n    />,\n  );\n}\n\nfunction placeProjectedImageFrame(\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 detachUnrenderedImageFrames(\n  cache: ImageFrameProjectionCache,\n  retainedFrameIndexes: ReadonlySet<number>,\n) {\n  for (const [frameIndex, frame] of cache.frames) {\n    if (retainedFrameIndexes.has(frameIndex)) continue;\n    frame.shell.remove();\n  }\n}\n\nfunction evictImageFrameProjectionCache(\n  cache: ImageFrameProjectionCache,\n  visibleFrameIndexes: ReadonlySet<number>,\n) {\n  let decodedPixels = 0;\n  let renderedPixels = 0;\n\n  for (const frame of cache.frames.values()) {\n    decodedPixels += frame.decodedPixels;\n    renderedPixels += frame.renderedPixels;\n  }\n\n  if (\n    decodedPixels <= IMAGE_RETAINED_DECODED_PIXEL_BUDGET &&\n    renderedPixels <= IMAGE_RETAINED_RENDERED_PIXEL_BUDGET\n  ) {\n    return;\n  }\n\n  const evictableFrames = [...cache.frames.entries()]\n    .filter(([frameIndex]) => !visibleFrameIndexes.has(frameIndex))\n    .sort(([, a], [, b]) => a.lastSeen - b.lastSeen);\n\n  for (const [frameIndex, frame] of evictableFrames) {\n    if (\n      decodedPixels <= IMAGE_RETAINED_DECODED_PIXEL_BUDGET &&\n      renderedPixels <= IMAGE_RETAINED_RENDERED_PIXEL_BUDGET\n    ) {\n      return;\n    }\n    decodedPixels -= frame.decodedPixels;\n    renderedPixels -= frame.renderedPixels;\n    disposeProjectedImageFrame(frame);\n    cache.frames.delete(frameIndex);\n  }\n}\n\ntype ImageFramePrefetchInput = {\n  frameNumbers: readonly number[];\n  layout: ImageFrameLayoutModel;\n  source: FrameSource;\n};\n\nfunction schedulePrefetchImageFrames(input: ImageFramePrefetchInput) {\n  if (input.frameNumbers.length === 0) return;\n  if (typeof requestAnimationFrame === \"function\") {\n    requestAnimationFrame(() => prefetchImageFrames(input));\n    return;\n  }\n  window.setTimeout(() => prefetchImageFrames(input), 0);\n}\n\nfunction prefetchImageFrames({\n  frameNumbers,\n  layout,\n  source,\n}: ImageFramePrefetchInput) {\n  if (frameNumbers.length === 0) return;\n  source.prefetch(\n    frameNumbers.flatMap((frameNumber) => {\n      const frame = getImageFrameLayout(layout, frameNumber);\n      return frame ? [frame.frameIndex] : [];\n    }),\n  );\n}\n\nfunction frameDecodedPixels(source: FrameSource, frameIndex: number) {\n  const size = source.frames[frameIndex]?.intrinsicSize;\n  return size ? size.width * size.height : 0;\n}\n\nfunction frameRenderedPixels(frame: ImageFrameLayout) {\n  const dpr = getImageDevicePixelRatio();\n  return (\n    Math.max(1, Math.floor(frame.width * dpr)) *\n    Math.max(1, Math.floor(frame.height * dpr))\n  );\n}\n\nfunction disposeImageFrameProjectionCache(cache: ImageFrameProjectionCache) {\n  for (const projectedFrame of cache.frames.values()) {\n    disposeProjectedImageFrame(projectedFrame);\n  }\n  cache.frames.clear();\n}\n\nfunction disposeProjectedImageFrame(projectedFrame: ProjectedImageFrame) {\n  if (!projectedFrame.isLive) return;\n  projectedFrame.isLive = false;\n  deferImageRootUnmount(projectedFrame.root);\n  projectedFrame.shell.remove();\n}\n\nfunction deferImageRootUnmount(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 getImageProjectionSourceKey(source: FrameSource) {\n  const cached = imageProjectionSourceKeys.get(source);\n  if (cached) return cached;\n  const key = nextImageProjectionSourceKey;\n  nextImageProjectionSourceKey += 1;\n  imageProjectionSourceKeys.set(source, key);\n  return key;\n}\n\nfunction getImageProjectionCallbackKey(\n  callback: ImageProjectionCallback | undefined,\n) {\n  if (!callback) return 0;\n  const cached = imageProjectionCallbackKeys.get(callback);\n  if (cached) return cached;\n  const key = nextImageProjectionCallbackKey;\n  nextImageProjectionCallbackKey += 1;\n  imageProjectionCallbackKeys.set(callback, key);\n  return key;\n}\n\nfunction resizeImageCanvas(\n  canvas: HTMLCanvasElement,\n  width: number,\n  height: number,\n  { preserve }: { preserve: boolean },\n) {\n  if (canvas.width === width && canvas.height === height) return null;\n  const shouldPreserve =\n    preserve && canvas.dataset.imageFrameRendered === \"true\";\n  const previousCanvas =\n    shouldPreserve && canvas.width > 0 && canvas.height > 0\n      ? copyCanvasContents(canvas)\n      : null;\n  if (canvas.width !== width) canvas.width = width;\n  if (canvas.height !== height) canvas.height = height;\n  return previousCanvas;\n}\n\nfunction copyCanvasContents(canvas: HTMLCanvasElement) {\n  const copy = document.createElement(\"canvas\");\n  copy.width = canvas.width;\n  copy.height = canvas.height;\n  const context = copy.getContext(\"2d\");\n  if (!context) return null;\n  try {\n    context.drawImage(canvas, 0, 0);\n  } catch {\n    return null;\n  }\n  return copy;\n}\n\nfunction preserveCanvasImage(\n  context: CanvasRenderingContext2D,\n  source: CanvasImageSource,\n  width: number,\n  height: number,\n) {\n  try {\n    context.drawImage(source, 0, 0, width, height);\n  } catch {\n    /* Best-effort preservation must not fail the real frame render. */\n  }\n}\n\nfunction getImageDevicePixelRatio() {\n  const dpr = typeof window === \"undefined\" ? 1 : window.devicePixelRatio;\n  return Number.isFinite(dpr) && dpr > 0 ? dpr : 1;\n}\n\nfunction setImagePixelStyle(\n  element: HTMLElement,\n  property: \"height\" | \"width\",\n  value: number,\n) {\n  setImageStyle(element, property, `${value}px`);\n}\n\nfunction setImageStyle(\n  element: HTMLElement,\n  property: \"height\" | \"transform\" | \"width\",\n  value: string,\n) {\n  if (element.style.getPropertyValue(property) === value) return;\n  element.style.setProperty(property, value);\n}\n\nfunction assignImageRef<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 notifyImageFrameRenderTiming(\n  callback: ((timing: ImageFrameRenderTiming) => void) | undefined,\n  timing: ImageFrameRenderTiming,\n) {\n  try {\n    callback?.(timing);\n  } catch {\n    /* Instrumentation callbacks must not affect frame rendering. */\n  }\n}\n\nfunction now() {\n  return typeof performance === \"undefined\" ? Date.now() : performance.now();\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer-frame.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer-chrome.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { ViewerControlsSkeleton } from \"@/components/ui/viewer-controls\";\n\nexport function ImageViewerFallback({\n  className,\n  bare = false,\n  fallbackFrameSize,\n  scale,\n  controls = true,\n}: {\n  className?: string;\n  bare?: boolean;\n  fallbackFrameSize?: { width: number; height: number };\n  scale?: 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=\"image-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          <ImageFrameSkeleton frameSize={fallbackFrameSize} scale={scale} />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction ImageFrameSkeleton({\n  frameSize,\n  scale,\n}: {\n  frameSize?: { width: number; height: number };\n  scale?: number;\n}) {\n  const normalizedScale =\n    scale !== undefined && Number.isFinite(scale) && scale > 0\n      ? Math.min(5, Math.max(0.1, scale))\n      : null;\n  const style: React.CSSProperties | undefined = frameSize\n    ? normalizedScale !== null\n      ? {\n          width: frameSize.width * normalizedScale,\n          height: frameSize.height * normalizedScale,\n        }\n      : {\n          aspectRatio: `${frameSize.width} / ${frameSize.height}`,\n          minWidth: frameSize.width * 0.1,\n        }\n    : { aspectRatio: \"4 / 3\" };\n\n  return (\n    <Skeleton\n      aria-hidden\n      className=\"ring-border w-full rounded-none shadow-sm ring-1\"\n      data-slot=\"image-frame-skeleton\"\n      style={style}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer-chrome.tsx"
    },
    {
      "path": "registry/new-york-v4/lib/image-geometry.ts",
      "content": "export type QuarterTurn = 0 | 90 | 180 | 270;\n\nexport interface Size {\n  width: number;\n  height: number;\n}\n\nexport interface NormalizedBox {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n}\n\nexport interface FrameOverlayProps {\n  frameNumber: number;\n  frameRect: Size;\n  scale: number;\n  rotation: QuarterTurn;\n}\n\nexport function normalizeRotation(rotation: number): QuarterTurn {\n  const normalized = ((rotation % 360) + 360) % 360;\n  if (normalized === 90 || normalized === 180 || normalized === 270) {\n    return normalized;\n  }\n  return 0;\n}\n\nexport function isRotatedSideways(rotation: QuarterTurn): boolean {\n  return rotation === 90 || rotation === 270;\n}\n\nexport function rotatedSize(size: Size, rotation: QuarterTurn): Size {\n  return isRotatedSideways(rotation)\n    ? { width: size.height, height: size.width }\n    : size;\n}\n\nexport function frameCssSize(\n  intrinsicSize: Size,\n  scale: number,\n  rotation: QuarterTurn,\n): Size {\n  const size = rotatedSize(intrinsicSize, rotation);\n  return {\n    width: size.width * scale,\n    height: size.height * scale,\n  };\n}\n\nexport function rotateNormalizedBox(\n  box: NormalizedBox,\n  rotation: QuarterTurn,\n): NormalizedBox {\n  if (rotation === 90) {\n    return {\n      left: 1 - box.top - box.height,\n      top: box.left,\n      width: box.height,\n      height: box.width,\n    };\n  }\n  if (rotation === 180) {\n    return {\n      left: 1 - box.left - box.width,\n      top: 1 - box.top - box.height,\n      width: box.width,\n      height: box.height,\n    };\n  }\n  if (rotation === 270) {\n    return {\n      left: box.top,\n      top: 1 - box.left - box.width,\n      width: box.height,\n      height: box.width,\n    };\n  }\n  return box;\n}\n\nexport function frameNumberToIndex(frameNumber: number): number {\n  if (!Number.isFinite(frameNumber)) return 0;\n  return Math.max(0, Math.floor(frameNumber) - 1);\n}\n\nexport function frameIndexToNumber(frameIndex: number): number {\n  return frameIndex + 1;\n}\n",
      "type": "registry:lib",
      "target": "@lib/image-geometry.ts"
    },
    {
      "path": "registry/new-york-v4/lib/image-frame-source.ts",
      "content": "import type { Size } from \"@/lib/image-geometry\";\nimport {\n  isViewerFormatError,\n  ViewerFormatError,\n  type ViewerFormatErrorMapperOptions,\n} from \"@/lib/viewer-errors\";\n\nexport class ImageLoadError extends ViewerFormatError {\n  constructor(message: string, options?: ErrorOptions) {\n    super({\n      format: \"image\",\n      kind: \"load_failed\",\n      message,\n      cause: options?.cause,\n    });\n    this.name = \"ImageLoadError\";\n  }\n}\n\nexport class ImageDecodeError extends ViewerFormatError {\n  constructor(message: string, options?: ErrorOptions) {\n    super({\n      format: \"image\",\n      kind: \"decode_failed\",\n      message,\n      cause: options?.cause,\n    });\n    this.name = \"ImageDecodeError\";\n  }\n}\n\nexport class ImageSourceDisposedError extends ViewerFormatError {\n  constructor(message = \"Image source disposed\", options?: ErrorOptions) {\n    super({\n      format: \"image\",\n      kind: \"disposed\",\n      message,\n      cause: options?.cause,\n    });\n    this.name = \"ImageSourceDisposedError\";\n  }\n}\n\nexport class ImageFrameIndexError extends ViewerFormatError {\n  constructor(frameIndex: number, frameCount: number) {\n    super({\n      format: \"image\",\n      kind: \"index_out_of_range\",\n      message: `Invalid image frame index ${frameIndex}; expected 0-${Math.max(0, frameCount - 1)}`,\n    });\n    this.name = \"ImageFrameIndexError\";\n  }\n}\n\nexport function toImageFormatError(\n  error: unknown,\n  options: ViewerFormatErrorMapperOptions,\n): ViewerFormatError {\n  if (isViewerFormatError(error)) return error;\n  if (options.kind === \"load_failed\") {\n    return new ImageLoadError(options.message, { cause: error });\n  }\n  if (options.kind === \"decode_failed\") {\n    return new ImageDecodeError(options.message, { cause: error });\n  }\n  if (options.kind === \"disposed\") {\n    return new ImageSourceDisposedError(options.message, { cause: error });\n  }\n  return new ViewerFormatError({\n    format: \"image\",\n    kind: options.kind,\n    message: options.message,\n    cause: error,\n  });\n}\n\nexport interface FrameDescriptor {\n  intrinsicSize: Size;\n}\n\nexport interface FrameSource {\n  kind: \"native-image\" | \"tiff\";\n  frames: readonly FrameDescriptor[];\n  acquire(frameIndex: number): Promise<ImageBitmap>;\n  hasDecodedFrame(frameIndex: number): boolean;\n  prefetch(frameIndexes: readonly number[]): void;\n  release(frameIndex: number): void;\n  dispose(reason?: Error): void;\n}\n\ninterface BitmapCacheOptions {\n  maxDecodedFrames: number;\n  maxDecodedPixels?: number;\n}\n\nexport class BitmapCache {\n  private readonly bitmaps = new Map<number, ImageBitmap>();\n  private readonly pinnedFrameCounts = new Map<number, number>();\n  private readonly frameRecency: number[] = [];\n  private decodedPixels = 0;\n\n  constructor(private readonly options: BitmapCacheOptions) {}\n\n  get(frameIndex: number): ImageBitmap | undefined {\n    const bitmap = this.bitmaps.get(frameIndex);\n    if (bitmap) this.touch(frameIndex);\n    return bitmap;\n  }\n\n  set(frameIndex: number, bitmap: ImageBitmap) {\n    const previousBitmap = this.bitmaps.get(frameIndex);\n    if (previousBitmap && previousBitmap !== bitmap) {\n      this.decodedPixels -= imageBitmapPixels(previousBitmap);\n      closeBitmap(previousBitmap);\n    }\n    if (!previousBitmap) {\n      this.decodedPixels += imageBitmapPixels(bitmap);\n    } else if (previousBitmap !== bitmap) {\n      this.decodedPixels += imageBitmapPixels(bitmap);\n    }\n    this.bitmaps.set(frameIndex, bitmap);\n    this.touch(frameIndex);\n    this.evict();\n  }\n\n  pin(frameIndex: number) {\n    this.pinnedFrameCounts.set(\n      frameIndex,\n      (this.pinnedFrameCounts.get(frameIndex) ?? 0) + 1,\n    );\n    this.touch(frameIndex);\n  }\n\n  unpin(frameIndex: number) {\n    const pinCount = this.pinnedFrameCounts.get(frameIndex) ?? 0;\n    if (pinCount <= 1) {\n      this.pinnedFrameCounts.delete(frameIndex);\n    } else {\n      this.pinnedFrameCounts.set(frameIndex, pinCount - 1);\n    }\n    this.evict();\n  }\n\n  has(frameIndex: number): boolean {\n    return this.bitmaps.has(frameIndex);\n  }\n\n  isPinned(frameIndex: number): boolean {\n    return (this.pinnedFrameCounts.get(frameIndex) ?? 0) > 0;\n  }\n\n  dispose() {\n    for (const bitmap of this.bitmaps.values()) closeBitmap(bitmap);\n    this.bitmaps.clear();\n    this.pinnedFrameCounts.clear();\n    this.frameRecency.length = 0;\n    this.decodedPixels = 0;\n  }\n\n  private touch(frameIndex: number) {\n    const position = this.frameRecency.indexOf(frameIndex);\n    if (position >= 0) this.frameRecency.splice(position, 1);\n    this.frameRecency.push(frameIndex);\n  }\n\n  private evict() {\n    for (const frameIndex of [...this.frameRecency]) {\n      if (\n        this.bitmaps.size <= this.options.maxDecodedFrames &&\n        this.decodedPixels <= this.maxDecodedPixels\n      ) {\n        break;\n      }\n      if (this.isPinned(frameIndex)) continue;\n      const bitmap = this.bitmaps.get(frameIndex);\n      if (bitmap) {\n        this.decodedPixels -= imageBitmapPixels(bitmap);\n        closeBitmap(bitmap);\n      }\n      this.bitmaps.delete(frameIndex);\n      this.frameRecency.splice(this.frameRecency.indexOf(frameIndex), 1);\n    }\n  }\n\n  private get maxDecodedPixels() {\n    return this.options.maxDecodedPixels ?? Number.POSITIVE_INFINITY;\n  }\n}\n\ninterface CreateFrameSourceOptions {\n  kind: FrameSource[\"kind\"];\n  frames: readonly FrameDescriptor[];\n  decode(frameIndex: number): Promise<ImageBitmap>;\n  cancelDecode?: (frameIndex: number, reason: Error) => void;\n  maxDecodedFrames: number;\n  maxDecodedPixels?: number;\n  initialBitmaps?: readonly InitialBitmap[];\n  onDispose?: (reason: Error) => void;\n}\n\ninterface InitialBitmap {\n  frameIndex: number;\n  bitmap: ImageBitmap;\n}\n\ninterface InflightFrameDecode {\n  promise: Promise<ImageBitmap>;\n  reject(error: Error): void;\n  pinCount: number;\n}\n\nexport function createFrameSource({\n  kind,\n  frames,\n  decode,\n  cancelDecode,\n  maxDecodedFrames,\n  maxDecodedPixels,\n  initialBitmaps = [],\n  onDispose,\n}: CreateFrameSourceOptions): FrameSource {\n  try {\n    validateFrameDescriptors(frames);\n  } catch (error) {\n    closeInitialBitmaps(initialBitmaps);\n    throw error;\n  }\n  const bitmapCache = new BitmapCache({ maxDecodedFrames, maxDecodedPixels });\n  const inflightDecodes = new Map<number, InflightFrameDecode>();\n  let disposed = false;\n\n  for (const { frameIndex, bitmap } of initialBitmaps) {\n    if (isValidFrameIndex(frameIndex, frames.length)) {\n      bitmapCache.set(frameIndex, bitmap);\n    } else {\n      closeBitmap(bitmap);\n    }\n  }\n\n  return {\n    kind,\n    frames,\n    acquire(frameIndex) {\n      if (disposed) return Promise.reject(new ImageSourceDisposedError());\n      if (!isValidFrameIndex(frameIndex, frames.length)) {\n        return Promise.reject(\n          new ImageFrameIndexError(frameIndex, frames.length),\n        );\n      }\n      bitmapCache.pin(frameIndex);\n      const bitmap = bitmapCache.get(frameIndex);\n      if (bitmap) return Promise.resolve(bitmap);\n\n      let inflight = inflightDecodes.get(frameIndex);\n      if (!inflight) {\n        let rejectInflight: (error: Error) => void = () => {};\n        const currentInflight = {\n          reject: rejectInflight,\n          pinCount: 0,\n        } as InflightFrameDecode;\n        const promise = new Promise<ImageBitmap>((resolve, reject) => {\n          rejectInflight = reject;\n          currentInflight.reject = reject;\n          let decodedPromise: Promise<ImageBitmap>;\n          try {\n            decodedPromise = decode(frameIndex);\n          } catch (error) {\n            decodedPromise = Promise.reject(error);\n          }\n          decodedPromise\n            .then((decodedBitmap) => {\n              if (inflightDecodes.get(frameIndex) !== currentInflight) {\n                closeBitmap(decodedBitmap);\n                reject(\n                  new ImageSourceDisposedError(\"Image frame decode canceled\"),\n                );\n                return;\n              }\n              inflightDecodes.delete(frameIndex);\n              if (disposed) {\n                closeBitmap(decodedBitmap);\n                reject(new ImageSourceDisposedError());\n                return;\n              }\n              bitmapCache.set(frameIndex, decodedBitmap);\n              resolve(decodedBitmap);\n            })\n            .catch((error) => {\n              if (inflightDecodes.get(frameIndex) === currentInflight) {\n                inflightDecodes.delete(frameIndex);\n                while (currentInflight.pinCount > 0) {\n                  currentInflight.pinCount -= 1;\n                  bitmapCache.unpin(frameIndex);\n                }\n              }\n              reject(toImageDecodeError(error));\n            });\n        });\n        currentInflight.promise = promise;\n        inflight = currentInflight;\n        inflightDecodes.set(frameIndex, inflight);\n      }\n      inflight.pinCount += 1;\n\n      return inflight.promise;\n    },\n    hasDecodedFrame(frameIndex) {\n      if (disposed) return false;\n      if (!isValidFrameIndex(frameIndex, frames.length)) return false;\n      return bitmapCache.has(frameIndex);\n    },\n    prefetch(frameIndexes) {\n      if (disposed) return;\n      for (const frameIndex of frameIndexes) {\n        if (!isValidFrameIndex(frameIndex, frames.length)) continue;\n        if (bitmapCache.has(frameIndex)) continue;\n        if (inflightDecodes.has(frameIndex)) continue;\n        void this.acquire(frameIndex)\n          .catch(() => undefined)\n          .finally(() => this.release(frameIndex));\n      }\n    },\n    release(frameIndex) {\n      if (!isValidFrameIndex(frameIndex, frames.length)) return;\n      if (disposed) return;\n      bitmapCache.unpin(frameIndex);\n      const inflight = inflightDecodes.get(frameIndex);\n      if (inflight) inflight.pinCount = Math.max(0, inflight.pinCount - 1);\n      if (\n        inflight &&\n        !bitmapCache.isPinned(frameIndex) &&\n        !bitmapCache.has(frameIndex)\n      ) {\n        const reason = new ImageSourceDisposedError(\n          \"Image frame decode canceled\",\n        );\n        inflight.reject(reason);\n        inflightDecodes.delete(frameIndex);\n        safeCancelDecode(cancelDecode, frameIndex, reason);\n      }\n    },\n    dispose(reason = new ImageSourceDisposedError()) {\n      if (disposed) return;\n      disposed = true;\n      bitmapCache.dispose();\n      for (const [frameIndex, inflight] of inflightDecodes) {\n        inflight.reject(reason);\n        safeCancelDecode(cancelDecode, frameIndex, reason);\n      }\n      inflightDecodes.clear();\n      onDispose?.(reason);\n    },\n  };\n}\n\nexport async function createNativeImageFrameSource(\n  bytes: ArrayBuffer,\n  contentType: string | null,\n  maxDecodedFrames: number,\n  maxDecodedPixels?: number,\n): Promise<FrameSource> {\n  const blob = new Blob([bytes], { type: contentType ?? \"\" });\n  return createNativeImageFrameSourceFromBlob(\n    blob,\n    maxDecodedFrames,\n    maxDecodedPixels,\n  );\n}\n\nexport async function createNativeImageFrameSourceFromBlob(\n  blob: Blob,\n  maxDecodedFrames: number,\n  maxDecodedPixels?: number,\n): Promise<FrameSource> {\n  let probe: ImageBitmap;\n  try {\n    probe = await createImageBitmap(blob);\n  } catch (error) {\n    throw toImageFormatError(error, {\n      kind: \"decode_failed\",\n      message: \"Failed to decode image\",\n    });\n  }\n  const frames: FrameDescriptor[] = [\n    { intrinsicSize: { width: probe.width, height: probe.height } },\n  ];\n  return createFrameSource({\n    kind: \"native-image\",\n    frames,\n    maxDecodedFrames,\n    maxDecodedPixels,\n    initialBitmaps: [{ frameIndex: 0, bitmap: probe }],\n    decode: () => createImageBitmap(blob),\n  });\n}\n\nexport function isDeclaredTiff(\n  src: string,\n  contentType: string | null,\n): boolean {\n  return /\\.tiff?($|[?#])/i.test(src) || isTiffContentType(contentType);\n}\n\nexport function isDeclaredNativeImage(\n  src: string,\n  contentType: string | null,\n): boolean {\n  if (isDeclaredTiff(src, contentType)) return false;\n  if (!contentType)\n    return /\\.(png|jpe?g|webp|gif|avif|bmp|ico)($|[?#])/i.test(src);\n  return /^image\\/(png|jpe?g|webp|gif|avif|bmp|x-icon|vnd\\.microsoft\\.icon)(;|$)/i.test(\n    contentType,\n  );\n}\n\nexport function isTiffBytes(\n  src: string,\n  contentType: string | null,\n  bytes: ArrayBuffer,\n): boolean {\n  if (isDeclaredTiff(src, contentType)) return true;\n  const b = new Uint8Array(bytes, 0, Math.min(4, bytes.byteLength));\n  return (\n    (b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a && b[3] === 0x00) ||\n    (b[0] === 0x4d && b[1] === 0x4d && b[2] === 0x00 && b[3] === 0x2a)\n  );\n}\n\nexport function closeBitmap(bitmap: ImageBitmap | undefined) {\n  try {\n    bitmap?.close();\n  } catch {\n    // Disposal must stay idempotent across browser edges and test doubles.\n  }\n}\n\nfunction imageBitmapPixels(bitmap: ImageBitmap) {\n  const pixels = bitmap.width * bitmap.height;\n  return Number.isFinite(pixels) && pixels > 0 ? pixels : 0;\n}\n\nfunction toImageDecodeError(error: unknown): ViewerFormatError {\n  if (error instanceof ImageSourceDisposedError) return error;\n  return error instanceof ImageDecodeError\n    ? error\n    : toImageFormatError(error, {\n        kind: \"decode_failed\",\n        message: \"Image decode failed\",\n      });\n}\n\nfunction safeCancelDecode(\n  cancelDecode: CreateFrameSourceOptions[\"cancelDecode\"] | undefined,\n  frameIndex: number,\n  reason: Error,\n) {\n  try {\n    cancelDecode?.(frameIndex, reason);\n  } catch {\n    // Local lifecycle cleanup has already happened; cancellation transport is best-effort.\n  }\n}\n\nfunction isValidFrameIndex(frameIndex: number, frameCount: number) {\n  return (\n    Number.isInteger(frameIndex) && frameIndex >= 0 && frameIndex < frameCount\n  );\n}\n\nfunction isTiffContentType(contentType: string | null): boolean {\n  return contentType ? /^image\\/(x-)?tiff?(;|$)/i.test(contentType) : false;\n}\n\nfunction validateFrameDescriptors(frames: readonly FrameDescriptor[]) {\n  if (frames.length === 0) {\n    throw new ImageDecodeError(\"Image does not contain any frames\");\n  }\n  for (const [frameIndex, frame] of frames.entries()) {\n    const { width, height } = frame.intrinsicSize;\n    if (\n      !Number.isFinite(width) ||\n      !Number.isFinite(height) ||\n      width <= 0 ||\n      height <= 0\n    ) {\n      throw new ImageDecodeError(\n        `Image frame ${frameIndex + 1} has invalid dimensions`,\n      );\n    }\n  }\n}\n\nfunction closeInitialBitmaps(initialBitmaps: readonly InitialBitmap[]) {\n  for (const { bitmap } of initialBitmaps) closeBitmap(bitmap);\n}\n",
      "type": "registry:lib",
      "target": "@lib/image-frame-source.ts"
    },
    {
      "path": "registry/new-york-v4/lib/image-source-cache.ts",
      "content": "import {\n  createNativeImageFrameSourceFromBlob,\n  ImageDecodeError,\n  ImageLoadError,\n  ImageSourceDisposedError,\n  isDeclaredNativeImage,\n  isDeclaredTiff,\n  isTiffBytes,\n  type FrameSource,\n} from \"@/lib/image-frame-source\";\nimport {\n  createTiffFrameSource,\n  type TiffWorkerFactory,\n} from \"@/lib/image-tiff-source\";\nimport type {\n  ViewerContentBlob,\n  ViewerContentBytes,\n  ViewerContentDirectUrl,\n  ViewerContentIdentity,\n  ViewerContentMime,\n  ViewerContentPayload,\n} from \"@/lib/viewer-resource\";\n\nconst DEFAULT_MAX_DECODED_FRAMES = 16;\nconst DEFAULT_MAX_DECODED_PIXELS = 64_000_000;\nconst DEFAULT_UNCLAIMED_SOURCE_TIMEOUT_MS = 30_000;\nconst DEFAULT_RELEASED_SOURCE_TIMEOUT_MS = 10_000;\nconst TIFF_SIGNATURE_BYTE_COUNT = 4;\n\nexport interface FrameSourceLease {\n  source: FrameSource;\n  release(): void;\n}\n\nexport type ImageSourceContent = ViewerContentIdentity &\n  ViewerContentDirectUrl &\n  ViewerContentMime &\n  ViewerContentPayload &\n  ViewerContentBlob &\n  ViewerContentBytes;\n\ntype FrameSourceEntryState = \"pending\" | \"resolved\" | \"evictable\" | \"disposed\";\n\ninterface FrameSourceEntry {\n  content: ImageSourceContent;\n  abortController: AbortController;\n  promise: Promise<FrameSource>;\n  source?: FrameSource;\n  leaseCount: number;\n  state: FrameSourceEntryState;\n  disposeReason?: Error;\n  disposeTimer?: ReturnType<typeof setTimeout>;\n}\n\ninterface FrameSourceManagerOptions {\n  maxDecodedFrames?: number;\n  maxDecodedPixels?: number;\n  releasedSourceTimeoutMs?: number;\n  unclaimedSourceTimeoutMs?: number;\n}\n\nexport class FrameSourceManager {\n  private readonly entries = new Map<string, FrameSourceEntry>();\n  private readonly maxDecodedFrames: number;\n  private readonly maxDecodedPixels: number;\n  private readonly releasedSourceTimeoutMs: number;\n  private readonly unclaimedSourceTimeoutMs: number;\n\n  constructor(options: FrameSourceManagerOptions = {}) {\n    this.maxDecodedFrames =\n      options.maxDecodedFrames ?? DEFAULT_MAX_DECODED_FRAMES;\n    this.maxDecodedPixels =\n      options.maxDecodedPixels ?? DEFAULT_MAX_DECODED_PIXELS;\n    this.releasedSourceTimeoutMs =\n      options.releasedSourceTimeoutMs ?? DEFAULT_RELEASED_SOURCE_TIMEOUT_MS;\n    this.unclaimedSourceTimeoutMs =\n      options.unclaimedSourceTimeoutMs ?? DEFAULT_UNCLAIMED_SOURCE_TIMEOUT_MS;\n  }\n\n  load(\n    content: ImageSourceContent,\n    createTiffWorker: TiffWorkerFactory,\n  ): Promise<FrameSource> {\n    const loadKey = content.key;\n    let entry = this.entries.get(loadKey);\n    if (!entry) {\n      const abortController = new AbortController();\n      const newEntry: FrameSourceEntry = {\n        content,\n        abortController,\n        promise: Promise.resolve().then(() =>\n          this.createSource(content, createTiffWorker, abortController.signal),\n        ),\n        leaseCount: 0,\n        state: \"pending\",\n      };\n      newEntry.promise = newEntry.promise\n        .then((source) => {\n          newEntry.source = source;\n          if (newEntry.state === \"disposed\") {\n            source.dispose(\n              newEntry.disposeReason ?? new ImageSourceDisposedError(),\n            );\n            this.entries.delete(loadKey);\n            newEntry.state = \"disposed\";\n            throw new ImageLoadError(\"Image source was disposed before use\");\n          }\n          if (newEntry.leaseCount === 0) {\n            newEntry.state = \"evictable\";\n            this.scheduleDispose(\n              newEntry,\n              this.unclaimedSourceTimeoutMs,\n              new ImageSourceDisposedError(),\n            );\n          } else {\n            newEntry.state = \"resolved\";\n          }\n          return source;\n        })\n        .catch((error) => {\n          if (this.entries.get(loadKey) === newEntry) {\n            this.entries.delete(loadKey);\n          }\n          if (\n            newEntry.state === \"disposed\" &&\n            error instanceof ImageDecodeError\n          ) {\n            throw newEntry.disposeReason ?? new ImageSourceDisposedError();\n          }\n          throw error;\n        });\n      entry = newEntry;\n      this.entries.set(loadKey, entry);\n    }\n    return entry.promise;\n  }\n\n  retain(\n    content: ViewerContentIdentity,\n    source: FrameSource,\n  ): FrameSourceLease | null {\n    const entry = this.entries.get(content.key);\n    if (!entry || entry.source !== source || entry.state === \"disposed\") {\n      return null;\n    }\n    this.cancelDispose(entry);\n    entry.state = \"resolved\";\n    entry.leaseCount += 1;\n    let hasReleased = false;\n    return {\n      source,\n      release: () => {\n        if (hasReleased) return;\n        hasReleased = true;\n        const current = this.entries.get(content.key);\n        if (!current || current.source !== source) return;\n        current.leaseCount = Math.max(0, current.leaseCount - 1);\n        if (current.leaseCount === 0) {\n          current.state = \"evictable\";\n          this.scheduleDispose(\n            current,\n            this.releasedSourceTimeoutMs,\n            new ImageSourceDisposedError(),\n          );\n        }\n      },\n    };\n  }\n\n  clear() {\n    for (const entry of [...this.entries.values()]) {\n      this.disposeEntry(entry, new ImageSourceDisposedError());\n    }\n  }\n\n  private async createSource(\n    content: ImageSourceContent,\n    createTiffWorker: TiffWorkerFactory,\n    signal: AbortSignal,\n  ): Promise<FrameSource> {\n    const sourceName = imageSourceName(content);\n    const declaredContentType = imageContentType(content);\n\n    if (isDeclaredTiff(sourceName, declaredContentType)) {\n      return createTiffFrameSource(\n        await readContentBytes(content, { signal }),\n        createTiffWorker,\n        this.maxDecodedFrames,\n        signal,\n        this.maxDecodedPixels,\n      );\n    }\n\n    if (isDeclaredNativeImage(sourceName, declaredContentType)) {\n      return createNativeImageFrameSourceFromBlob(\n        await content.readBlob({ signal }),\n        this.maxDecodedFrames,\n        this.maxDecodedPixels,\n      );\n    }\n\n    const blob = await content.readBlob({ signal });\n    return this.createSourceFromUnknownBlob(\n      sourceName,\n      blob,\n      createTiffWorker,\n      signal,\n    );\n  }\n\n  private disposeEntry(entry: FrameSourceEntry, reason: Error) {\n    if (entry.state === \"disposed\") return;\n    entry.state = \"disposed\";\n    entry.disposeReason = reason;\n    this.cancelDispose(entry);\n    entry.abortController.abort(reason);\n    entry.source?.dispose(reason);\n    if (this.entries.get(entry.content.key) === entry) {\n      this.entries.delete(entry.content.key);\n    }\n  }\n\n  private scheduleDispose(\n    entry: FrameSourceEntry,\n    delayMs: number,\n    reason: Error,\n  ) {\n    this.cancelDispose(entry);\n    entry.disposeReason = reason;\n    entry.disposeTimer = setTimeout(() => {\n      const current = this.entries.get(entry.content.key);\n      if (!current || current !== entry || current.leaseCount > 0) return;\n      this.disposeEntry(current, reason);\n    }, delayMs);\n  }\n\n  private cancelDispose(entry: FrameSourceEntry) {\n    if (!entry.disposeTimer) return;\n    clearTimeout(entry.disposeTimer);\n    entry.disposeTimer = undefined;\n  }\n\n  private async createSourceFromUnknownBlob(\n    sourceName: string,\n    blob: Blob,\n    createTiffWorker: TiffWorkerFactory,\n    signal: AbortSignal,\n  ): Promise<FrameSource> {\n    const contentType = blob.type || null;\n    const prefix = await blob.slice(0, TIFF_SIGNATURE_BYTE_COUNT).arrayBuffer();\n    if (isTiffBytes(sourceName, contentType, prefix)) {\n      return createTiffFrameSource(\n        await blob.arrayBuffer(),\n        createTiffWorker,\n        this.maxDecodedFrames,\n        signal,\n        this.maxDecodedPixels,\n      );\n    }\n\n    return createNativeImageFrameSourceFromBlob(\n      blob,\n      this.maxDecodedFrames,\n      this.maxDecodedPixels,\n    );\n  }\n}\n\nexport const imageFrameSourceManager = new FrameSourceManager();\n\nfunction imageSourceName(content: ViewerContentDirectUrl): string {\n  return content.directUrl ?? \"\";\n}\n\nfunction imageContentType(\n  content: ViewerContentPayload & ViewerContentMime,\n): string | null {\n  if (content.mimeType) return content.mimeType;\n  if (content.payload.kind === \"blob\") return content.payload.blob.type || null;\n  return null;\n}\n\nfunction readContentBytes(\n  content: ViewerContentBytes,\n  options: { signal: AbortSignal },\n): Promise<ArrayBuffer> {\n  return content.readBytes(options);\n}\n",
      "type": "registry:lib",
      "target": "@lib/image-source-cache.ts"
    },
    {
      "path": "registry/new-york-v4/lib/image-tiff-source.ts",
      "content": "import {\n  closeBitmap,\n  createFrameSource,\n  ImageDecodeError,\n  ImageSourceDisposedError,\n  type FrameDescriptor,\n  type FrameSource,\n} from \"@/lib/image-frame-source\";\nimport { ViewerFormatError } from \"@/lib/viewer-errors\";\n\nexport class TiffWorkerError extends ViewerFormatError {\n  constructor(message: string, options?: ErrorOptions) {\n    super({\n      format: \"image\",\n      kind: \"worker_failed\",\n      message,\n      cause: options?.cause,\n    });\n    this.name = \"TiffWorkerError\";\n  }\n}\n\nexport type TiffWorkerRequest =\n  | { type: \"init\"; buffer: ArrayBuffer }\n  | { type: \"decodeFrame\"; requestId: number; frameIndex: number }\n  | { type: \"cancelDecode\"; requestId: number };\n\nexport type TiffWorkerResponse =\n  | { type: \"initOk\"; frames: FrameDescriptor[] }\n  | { type: \"initError\"; message: string }\n  | { type: \"decodeFrameOk\"; requestId: number; bitmap: ImageBitmap }\n  | { type: \"decodeFrameError\"; requestId: number; message: string };\n\nexport type TiffWorkerFactory = () => Worker;\n\ninterface PendingDecode {\n  frameIndex: number;\n  resolve(bitmap: ImageBitmap): void;\n  reject(error: Error): void;\n}\n\nexport class TiffWorkerClient {\n  private readonly worker: Worker;\n  private readonly pendingDecodes = new Map<number, PendingDecode>();\n  private initResolve: ((frames: readonly FrameDescriptor[]) => void) | null =\n    null;\n  private initReject: ((error: Error) => void) | null = null;\n  private initAbortCleanup: (() => void) | null = null;\n  private nextRequestId = 0;\n  private initialized = false;\n  private disposed = false;\n\n  constructor(createWorker: TiffWorkerFactory) {\n    this.worker = createWorker();\n    this.worker.onmessage = (event: MessageEvent<TiffWorkerResponse>) => {\n      this.handleMessage(event.data);\n    };\n    this.worker.onerror = (event) => {\n      this.fail(new TiffWorkerError(event.message || \"TIFF worker failed\"));\n    };\n    this.worker.onmessageerror = () => {\n      this.fail(new TiffWorkerError(\"TIFF worker sent an unreadable message\"));\n    };\n  }\n\n  init(\n    buffer: ArrayBuffer,\n    signal?: AbortSignal,\n  ): Promise<readonly FrameDescriptor[]> {\n    if (this.disposed) {\n      return Promise.reject(new TiffWorkerError(\"TIFF worker disposed\"));\n    }\n    if (this.initResolve) {\n      return Promise.reject(\n        new TiffWorkerError(\"TIFF worker already initializing\"),\n      );\n    }\n    if (this.initialized) {\n      return Promise.reject(\n        new TiffWorkerError(\"TIFF worker already initialized\"),\n      );\n    }\n    return new Promise((resolve, reject) => {\n      const cleanup = () => {\n        this.initAbortCleanup?.();\n        this.initAbortCleanup = null;\n      };\n      this.initResolve = (frames) => {\n        cleanup();\n        resolve(frames);\n      };\n      this.initReject = (error) => {\n        cleanup();\n        reject(error);\n      };\n      if (signal) {\n        const abort = () => {\n          this.dispose(abortSignalReason(signal));\n        };\n        if (signal.aborted) {\n          abort();\n          return;\n        }\n        signal.addEventListener(\"abort\", abort, { once: true });\n        this.initAbortCleanup = () => {\n          signal.removeEventListener(\"abort\", abort);\n        };\n      }\n      try {\n        this.worker.postMessage({ type: \"init\", buffer }, [buffer]);\n      } catch (error) {\n        const workerError = new TiffWorkerError(\n          \"Failed to initialize TIFF worker\",\n          {\n            cause: error,\n          },\n        );\n        this.disposed = true;\n        this.rejectInit(workerError);\n        this.rejectPending(workerError);\n        this.worker.terminate();\n      }\n    });\n  }\n\n  decode(frameIndex: number): Promise<ImageBitmap> {\n    if (this.disposed) {\n      return Promise.reject(new TiffWorkerError(\"TIFF worker disposed\"));\n    }\n    return new Promise((resolve, reject) => {\n      const requestId = this.nextRequestId++;\n      this.pendingDecodes.set(requestId, { frameIndex, resolve, reject });\n      try {\n        this.worker.postMessage({ type: \"decodeFrame\", requestId, frameIndex });\n      } catch (error) {\n        const workerError = new TiffWorkerError(\n          \"Failed to request TIFF frame decode\",\n          {\n            cause: error,\n          },\n        );\n        this.pendingDecodes.delete(requestId);\n        this.disposed = true;\n        this.rejectPending(workerError);\n        this.worker.terminate();\n        reject(workerError);\n      }\n    });\n  }\n\n  cancelDecode(\n    frameIndex: number,\n    reason: Error = new TiffWorkerError(\"TIFF decode canceled\"),\n  ) {\n    for (const [requestId, pending] of this.pendingDecodes) {\n      if (pending.frameIndex !== frameIndex) continue;\n      this.pendingDecodes.delete(requestId);\n      pending.reject(reason);\n      try {\n        this.worker.postMessage({ type: \"cancelDecode\", requestId });\n      } catch {\n        // The local promise is already rejected; worker transport may be gone.\n      }\n    }\n  }\n\n  dispose(reason: Error = new TiffWorkerError(\"TIFF worker disposed\")) {\n    if (this.disposed) return;\n    this.disposed = true;\n    this.rejectInit(reason);\n    this.rejectPending(reason);\n    this.worker.terminate();\n  }\n\n  private handleMessage(message: TiffWorkerResponse) {\n    if (message.type === \"initOk\") {\n      if (!isFrameDescriptorList(message.frames)) {\n        this.fail(\n          new TiffWorkerError(\"TIFF worker sent an invalid init response\"),\n        );\n        return;\n      }\n      try {\n        validateWorkerFrameDescriptors(message.frames);\n      } catch (error) {\n        this.fail(\n          error instanceof Error\n            ? error\n            : new ImageDecodeError(\"Image decode failed\"),\n        );\n        return;\n      }\n      this.initialized = true;\n      this.initResolve?.(message.frames);\n      this.initResolve = null;\n      this.initReject = null;\n      return;\n    }\n    if (message.type === \"initError\") {\n      if (typeof message.message !== \"string\") {\n        this.fail(\n          new TiffWorkerError(\"TIFF worker sent an invalid init response\"),\n        );\n        return;\n      }\n      const error = new TiffWorkerError(message.message);\n      this.disposed = true;\n      this.rejectPending(error);\n      this.rejectInit(error);\n      this.worker.terminate();\n      return;\n    }\n    if (message.type === \"decodeFrameOk\") {\n      if (\n        !isWorkerRequestId(message.requestId) ||\n        !isImageBitmap(message.bitmap)\n      ) {\n        closeBitmapLike(message.bitmap);\n        this.fail(\n          new TiffWorkerError(\"TIFF worker sent an invalid decode response\"),\n        );\n        return;\n      }\n      const pending = this.pendingDecodes.get(message.requestId);\n      this.pendingDecodes.delete(message.requestId);\n      if (pending) pending.resolve(message.bitmap);\n      else closeBitmap(message.bitmap);\n      return;\n    }\n    if (message.type === \"decodeFrameError\") {\n      if (\n        !isWorkerRequestId(message.requestId) ||\n        typeof message.message !== \"string\"\n      ) {\n        this.fail(\n          new TiffWorkerError(\"TIFF worker sent an invalid decode response\"),\n        );\n        return;\n      }\n      const pending = this.pendingDecodes.get(message.requestId);\n      this.pendingDecodes.delete(message.requestId);\n      pending?.reject(new ImageDecodeError(message.message));\n      return;\n    }\n    this.fail(new TiffWorkerError(\"TIFF worker sent an unknown message\"));\n  }\n\n  private fail(error: Error) {\n    if (this.disposed) return;\n    this.disposed = true;\n    this.rejectPending(error);\n    this.rejectInit(error);\n    this.worker.terminate();\n  }\n\n  private rejectInit(error: Error) {\n    this.initReject?.(error);\n    this.initResolve = null;\n    this.initReject = null;\n  }\n\n  private rejectPending(error: Error) {\n    for (const pending of this.pendingDecodes.values()) pending.reject(error);\n    this.pendingDecodes.clear();\n  }\n}\n\nexport async function createTiffFrameSource(\n  buffer: ArrayBuffer,\n  createWorker: TiffWorkerFactory,\n  maxDecodedFrames: number,\n  signal?: AbortSignal,\n  maxDecodedPixels?: number,\n): Promise<FrameSource> {\n  const client = new TiffWorkerClient(createWorker);\n  const frames = await client.init(buffer, signal);\n  try {\n    return createFrameSource({\n      kind: \"tiff\",\n      frames,\n      maxDecodedFrames,\n      maxDecodedPixels,\n      decode: (frameIndex) => client.decode(frameIndex),\n      cancelDecode: (frameIndex, reason) =>\n        client.cancelDecode(frameIndex, reason),\n      onDispose: (reason) => client.dispose(reason),\n    });\n  } catch (error) {\n    client.dispose(error instanceof Error ? error : undefined);\n    throw error;\n  }\n}\n\nfunction abortSignalReason(signal: AbortSignal): Error {\n  return signal.reason instanceof Error\n    ? signal.reason\n    : new ImageSourceDisposedError();\n}\n\nfunction isWorkerRequestId(value: unknown): value is number {\n  return typeof value === \"number\" && Number.isInteger(value) && value >= 0;\n}\n\nfunction isImageBitmap(value: unknown): value is ImageBitmap {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    typeof (value as Partial<ImageBitmap>).close === \"function\" &&\n    Number.isFinite((value as Partial<ImageBitmap>).width) &&\n    Number.isFinite((value as Partial<ImageBitmap>).height) &&\n    (value as Partial<ImageBitmap>).width! > 0 &&\n    (value as Partial<ImageBitmap>).height! > 0\n  );\n}\n\nfunction closeBitmapLike(value: unknown) {\n  if (\n    typeof value === \"object\" &&\n    value !== null &&\n    typeof (value as Partial<ImageBitmap>).close === \"function\"\n  ) {\n    closeBitmap(value as ImageBitmap);\n  }\n}\n\nfunction isFrameDescriptorList(value: unknown): value is FrameDescriptor[] {\n  return (\n    Array.isArray(value) &&\n    value.every((frame) => {\n      if (typeof frame !== \"object\" || frame === null) return false;\n      const intrinsicSize = (frame as Partial<FrameDescriptor>).intrinsicSize;\n      return (\n        typeof intrinsicSize === \"object\" &&\n        intrinsicSize !== null &&\n        typeof intrinsicSize.width === \"number\" &&\n        typeof intrinsicSize.height === \"number\"\n      );\n    })\n  );\n}\n\nfunction validateWorkerFrameDescriptors(frames: readonly FrameDescriptor[]) {\n  if (frames.length === 0) {\n    throw new ImageDecodeError(\"Image does not contain any frames\");\n  }\n  for (const [frameIndex, frame] of frames.entries()) {\n    const { width, height } = frame.intrinsicSize;\n    if (\n      !Number.isFinite(width) ||\n      !Number.isFinite(height) ||\n      width <= 0 ||\n      height <= 0\n    ) {\n      throw new ImageDecodeError(\n        `Image frame ${frameIndex + 1} has invalid dimensions`,\n      );\n    }\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/image-tiff-source.ts"
    },
    {
      "path": "registry/new-york-v4/ui/image-viewer.worker.ts",
      "content": "// Web Worker that owns TIFF decode (UTIF) so it never blocks the UI thread.\n// The main thread transfers file bytes in once; the worker parses frame metadata\n// up front, decodes requested frames on demand, and transfers ImageBitmaps back.\nimport UTIF from \"utif\";\n\nimport type {\n  TiffWorkerRequest,\n  TiffWorkerResponse,\n} from \"@/lib/image-tiff-source\";\n\ninterface Ifd {\n  t256?: number[];\n  t257?: number[];\n  width?: number;\n  height?: number;\n  data?: unknown;\n}\n\ninterface Utif {\n  decode(buf: ArrayBuffer): Ifd[];\n  decodeImage(buf: ArrayBuffer, ifd: Ifd): void;\n  toRGBA8(ifd: Ifd): Uint8Array;\n}\n\nconst utif = UTIF as Utif;\nconst ctx = self as unknown as Worker;\n\nlet buffer: ArrayBuffer | null = null;\nlet ifds: Ifd[] = [];\nconst canceledDecodeRequestIds = new Set<number>();\nconst decodeQueue: { requestId: number; frameIndex: number }[] = [];\nlet isDecoding = false;\n\nctx.onmessage = async (event: MessageEvent<TiffWorkerRequest>) => {\n  const message = event.data;\n\n  if (message.type === \"init\") {\n    initialize(message.buffer);\n    return;\n  }\n\n  if (message.type === \"cancelDecode\") {\n    canceledDecodeRequestIds.add(message.requestId);\n    return;\n  }\n\n  decodeQueue.push({\n    requestId: message.requestId,\n    frameIndex: message.frameIndex,\n  });\n  scheduleDecodeQueue();\n};\n\nfunction scheduleDecodeQueue() {\n  if (isDecoding) return;\n  setTimeout(processDecodeQueue, 0);\n}\n\nasync function processDecodeQueue() {\n  if (isDecoding) return;\n  const next = decodeQueue.shift();\n  if (!next) return;\n  if (canceledDecodeRequestIds.delete(next.requestId)) {\n    scheduleDecodeQueue();\n    return;\n  }\n  isDecoding = true;\n  try {\n    await decodeFrame(next.requestId, next.frameIndex);\n  } finally {\n    isDecoding = false;\n    if (decodeQueue.length > 0) scheduleDecodeQueue();\n  }\n}\n\nfunction initialize(nextBuffer: ArrayBuffer) {\n  try {\n    buffer = nextBuffer;\n    ifds = utif.decode(buffer);\n    if (ifds.length === 0) throw new Error(\"TIFF has no frames\");\n    post({\n      type: \"initOk\",\n      frames: ifds.map((ifd, frameIndex) => {\n        const width = frameWidth(ifd);\n        const height = frameHeight(ifd);\n        if (!width || !height) {\n          throw new Error(`TIFF frame ${frameIndex + 1} has no dimensions`);\n        }\n        return { intrinsicSize: { width, height } };\n      }),\n    });\n  } catch (error) {\n    post({ type: \"initError\", message: errorMessage(error) });\n  }\n}\n\nasync function decodeFrame(requestId: number, frameIndex: number) {\n  let didDecode = false;\n  let ifd: Ifd | undefined;\n  try {\n    if (!buffer) throw new Error(\"worker not initialized\");\n    if (canceledDecodeRequestIds.delete(requestId)) return;\n    if (frameIndex < 0 || frameIndex >= ifds.length) {\n      throw new Error(`TIFF frame ${frameIndex + 1} is out of range`);\n    }\n    ifd = ifds[frameIndex];\n    const width = frameWidth(ifd);\n    const height = frameHeight(ifd);\n    if (!width || !height) {\n      throw new Error(`TIFF frame ${frameIndex + 1} has no dimensions`);\n    }\n\n    utif.decodeImage(buffer, ifd);\n    didDecode = true;\n    const rgba = utif.toRGBA8(ifd);\n    const image = new ImageData(new Uint8ClampedArray(rgba), width, height);\n    const bitmap = await createImageBitmap(image);\n    if (canceledDecodeRequestIds.delete(requestId)) {\n      bitmap.close();\n      return;\n    }\n    post({ type: \"decodeFrameOk\", requestId, bitmap }, [bitmap]);\n  } catch (error) {\n    post({\n      type: \"decodeFrameError\",\n      requestId,\n      message: `TIFF frame ${frameIndex + 1}: ${errorMessage(error)}`,\n    });\n  } finally {\n    canceledDecodeRequestIds.delete(requestId);\n    if (didDecode && ifd) delete ifd.data;\n  }\n}\n\nfunction post(message: TiffWorkerResponse, transfer?: Transferable[]): void {\n  ctx.postMessage(message, transfer ?? []);\n}\n\nfunction frameWidth(ifd: Ifd): number {\n  return ifd.width ?? ifd.t256?.[0] ?? 0;\n}\n\nfunction frameHeight(ifd: Ifd): number {\n  return ifd.height ?? ifd.t257?.[0] ?? 0;\n}\n\nfunction errorMessage(error: unknown): string {\n  return error instanceof Error ? error.message : \"TIFF decode failed\";\n}\n",
      "type": "registry:ui",
      "target": "@ui/image-viewer.worker.ts"
    },
    {
      "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/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/utif.d.ts",
      "content": "// Ambient shim for the `utif` package, which ships no type declarations\n// (and has no @types/utif on npm). shadcn's registry build strips leading\n// comments from source files, so an inline `@ts-expect-error` above the import\n// does not survive into the published item — a real declaration is required for\n// a fresh consumer install to typecheck. The decode workers cast the default\n// export to their own local shapes, so an opaque `any` default is sufficient.\ndeclare module \"utif\" {\n   \n  const UTIF: any;\n  export default UTIF;\n}\n",
      "type": "registry:ui",
      "target": "@ui/utif.d.ts"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-document-scroll.ts",
      "content": "import * as React from \"react\";\n\nimport { useKeyedLayoutEffect } from \"@/hooks/use-keyed-layout-effect\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { useMountEffect } from \"@/hooks/use-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nimport type {\n  ViewerDocumentLayoutModel,\n  ViewerDocumentPhysicalScrollPosition,\n  ViewerDocumentResolvedScrollTarget,\n  ViewerDocumentScrollMapper,\n  ViewerDocumentScrollMetrics,\n  ViewerDocumentScrollTargetResolver,\n  ViewerDocumentZoomMotionController,\n} from \"./viewer-types\";\nimport { useViewerDocumentGeometryTransaction } from \"./viewer-document-geometry\";\n\nconst VIEWER_DOCUMENT_SCROLL_IDLE_MS = 120;\nconst VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON = 1;\nconst VIEWER_DOCUMENT_SCROLL_RESTORE_MAX_FRAMES = 45;\nconst VIEWER_DOCUMENT_INTERNAL_SCROLL_SUPPRESS_MS = 120;\n// A zoom intent is consumed by the geometry 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 geometry change.\nconst VIEWER_DOCUMENT_ZOOM_INTENT_MAX_AGE_MS = 400;\n\ntype ViewerDocumentScrollIntent<Target> =\n  | {\n      kind: \"idle\";\n    }\n  | {\n      kind: \"user\";\n    }\n  | {\n      behavior: ScrollBehavior;\n      kind: \"programmatic\";\n      sequence: number;\n      target: Target;\n      targetLeft?: number;\n      targetTop: number;\n    };\n\ntype PendingViewerDocumentScrollRestore = {\n  attempts: number;\n  options: ScrollToOptions;\n  targetTop: number;\n};\n\ntype ViewerDocumentScrollEventResult = {\n  source: \"internal\" | \"programmatic\" | \"user\";\n};\n\nexport function useViewerDocumentScroll<Anchor, Target, ZoomTransaction = unknown>({\n  copyScrollTarget,\n  layout,\n  resetKey,\n  resolveScrollTarget,\n  scrollMapper,\n  zoomMotion,\n}: {\n  copyScrollTarget?: (target: Target) => Target;\n  layout: ViewerDocumentLayoutModel<Anchor>;\n  resetKey?: unknown;\n  resolveScrollTarget?: ViewerDocumentScrollTargetResolver<Anchor, Target>;\n  scrollMapper: ViewerDocumentScrollMapper;\n  zoomMotion?: ViewerDocumentZoomMotionController<ZoomTransaction>;\n}) {\n  const viewportElementRef = React.useRef<HTMLDivElement | null>(null);\n  const scrollPageOffsetRef = React.useRef(0);\n  const pendingZoomIntentRef = React.useRef<{\n    capturedAt: number;\n    transaction: ZoomTransaction;\n  } | null>(null);\n  const activeZoomMotionCancelRef = React.useRef<(() => void) | null>(null);\n  const pendingScrollRestoreRef =\n    React.useRef<PendingViewerDocumentScrollRestore | null>(null);\n  const pendingScrollRestoreFrameRef = React.useRef(0);\n  const flushPendingScrollRestoreRef = React.useRef<() => void>(() => {});\n  const isInternalScrollWriteRef = React.useRef(false);\n  const internalScrollWriteTimeoutRef = React.useRef<ReturnType<\n    typeof setTimeout\n  > | null>(null);\n  const viewportResetKeyRef = React.useRef<unknown>(resetKey);\n  const didMountResetEffectRef = React.useRef(false);\n  const committedLayoutRef = React.useRef(layout);\n  const committedResetKeyRef = React.useRef<unknown>(resetKey);\n  const geometryTransaction = useViewerDocumentGeometryTransaction({\n    layout,\n    resetKey,\n  });\n  const {\n    cacheAnchor,\n    cacheReadingAnchor,\n    cacheVisualLayerRect,\n    cancel: cancelGeometryTransaction,\n    classifyLayoutChange,\n    commit,\n    prepare,\n    recordStableState,\n    reset: resetGeometryTransaction,\n  } = geometryTransaction;\n  const scrollIntent = useViewerDocumentScrollIntentController<Target>();\n  const [viewportElement, setViewportElementState] =\n    React.useState<HTMLDivElement | null>(null);\n\n  const cancelPendingScrollRestore = React.useCallback(() => {\n    pendingScrollRestoreRef.current = null;\n    if (pendingScrollRestoreFrameRef.current === 0) return;\n    cancelAnimationFrame(pendingScrollRestoreFrameRef.current);\n    pendingScrollRestoreFrameRef.current = 0;\n  }, []);\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  const captureZoomIntent = React.useCallback(() => {\n    const viewportElement = viewportElementRef.current;\n    if (!viewportElement || !zoomMotion) {\n      pendingZoomIntentRef.current = null;\n      return;\n    }\n    const metrics = readViewerDocumentScrollMetrics({\n      layout,\n      scrollMapper,\n      scrollPageOffset: scrollPageOffsetRef.current,\n      viewportElement,\n    });\n    const transaction = zoomMotion.capture({\n      scrollTop: metrics.scrollTop,\n      viewportElement,\n    });\n    pendingZoomIntentRef.current =\n      transaction == null\n        ? null\n        : { capturedAt: readViewerDocumentNow(), transaction };\n  }, [layout, scrollMapper, zoomMotion]);\n\n  const clearInternalScrollWrite = React.useCallback(() => {\n    isInternalScrollWriteRef.current = false;\n    if (internalScrollWriteTimeoutRef.current === null) return;\n    clearTimeout(internalScrollWriteTimeoutRef.current);\n    internalScrollWriteTimeoutRef.current = null;\n  }, []);\n\n  const markInternalScrollWrite = React.useCallback(() => {\n    isInternalScrollWriteRef.current = true;\n    if (internalScrollWriteTimeoutRef.current !== null) {\n      clearTimeout(internalScrollWriteTimeoutRef.current);\n    }\n    internalScrollWriteTimeoutRef.current = setTimeout(() => {\n      internalScrollWriteTimeoutRef.current = null;\n      isInternalScrollWriteRef.current = false;\n    }, VIEWER_DOCUMENT_INTERNAL_SCROLL_SUPPRESS_MS);\n  }, []);\n\n  const resetViewportForKey = React.useCallback(\n    (element: HTMLDivElement, key: unknown) => {\n      cancelPendingScrollRestore();\n      cancelZoomMotion();\n      viewportResetKeyRef.current = key;\n      scrollPageOffsetRef.current = 0;\n      resetGeometryTransaction(element);\n      scrollIntent.reset();\n      markInternalScrollWrite();\n      setViewportPhysicalScrollTop(element, 0);\n      element.scrollTo?.({ top: 0, behavior: \"auto\" });\n    },\n    [\n      cancelPendingScrollRestore,\n      cancelZoomMotion,\n      markInternalScrollWrite,\n      resetGeometryTransaction,\n      scrollIntent,\n    ],\n  );\n\n  const setViewportElement = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      viewportElementRef.current = element;\n      if (element && !Object.is(viewportResetKeyRef.current, resetKey)) {\n        resetViewportForKey(element, resetKey);\n      }\n      setViewportElementState(element);\n    },\n    [resetKey, resetViewportForKey],\n  );\n\n  const readScrollMetrics = React.useCallback(\n    () =>\n      readViewerDocumentScrollMetrics({\n        layout,\n        scrollMapper,\n        scrollPageOffset: scrollPageOffsetRef.current,\n        viewportElement: viewportElementRef.current,\n      }),\n    [layout, scrollMapper],\n  );\n\n  const syncViewportScrollPosition = React.useCallback(\n    (viewportElement: HTMLDivElement, blockSize = layout.blockSize) => {\n      const metrics = readViewerDocumentScrollMetrics({\n        layout: {\n          ...layout,\n          blockSize,\n        },\n        scrollMapper,\n        scrollPageOffset: scrollPageOffsetRef.current,\n        viewportElement,\n      });\n\n      if (metrics.physicalScrollSize >= blockSize) {\n        const nextMetrics = {\n          ...metrics,\n          scrollPageOffset: hasViewerDocumentScrollPageOffset(\n            scrollPageOffsetRef.current,\n          )\n            ? metrics.scrollPageOffset\n            : 0,\n        };\n        scrollPageOffsetRef.current = nextMetrics.scrollPageOffset;\n        cacheReadingAnchor(nextMetrics);\n        cacheVisualLayerRect(viewportElement);\n        return nextMetrics;\n      }\n\n      const position = scrollMapper.resolvePhysicalScrollPosition({\n        blockSize,\n        logicalScrollTop: metrics.scrollTop,\n        scrollPageOffset: metrics.scrollPageOffset,\n        viewportBlockSize: metrics.viewportBlockSize,\n      });\n      scrollPageOffsetRef.current = position.scrollPageOffset;\n      markInternalScrollWrite();\n      setViewportPhysicalScrollTop(viewportElement, position.physicalScrollTop);\n\n      const nextMetrics = {\n        ...metrics,\n        physicalScrollTop: position.physicalScrollTop,\n        scrollPageOffset: position.scrollPageOffset,\n      };\n      cacheReadingAnchor(nextMetrics);\n      cacheVisualLayerRect(viewportElement);\n      return nextMetrics;\n    },\n    [\n      cacheReadingAnchor,\n      cacheVisualLayerRect,\n      layout,\n      markInternalScrollWrite,\n      scrollMapper,\n    ],\n  );\n\n  const syncScrollPosition = React.useCallback(() => {\n    const viewportElement = viewportElementRef.current;\n    return viewportElement ? syncViewportScrollPosition(viewportElement) : null;\n  }, [syncViewportScrollPosition]);\n\n  const schedulePendingScrollRestore = React.useCallback(\n    (restore: PendingViewerDocumentScrollRestore) => {\n      pendingScrollRestoreRef.current = restore;\n      if (pendingScrollRestoreFrameRef.current !== 0) return;\n      pendingScrollRestoreFrameRef.current = requestAnimationFrame(() => {\n        pendingScrollRestoreFrameRef.current = 0;\n        flushPendingScrollRestoreRef.current();\n      });\n    },\n    [],\n  );\n\n  const resolveLogicalScrollPosition = React.useCallback(\n    (viewportElement: HTMLDivElement, targetTop: number) => {\n      const viewportBlockSize = viewportElement.clientHeight;\n      const position = scrollMapper.resolvePhysicalScrollPosition({\n        blockSize: layout.blockSize,\n        logicalScrollTop: targetTop,\n        scrollPageOffset: scrollPageOffsetRef.current,\n        viewportBlockSize,\n      });\n      const expectedPhysicalScrollSize = scrollMapper.getPhysicalScrollSize({\n        blockSize: layout.blockSize,\n        viewportBlockSize,\n      });\n\n      return {\n        expectedMaxPhysicalScrollTop: Math.max(\n          0,\n          expectedPhysicalScrollSize - viewportBlockSize,\n        ),\n        position,\n      };\n    },\n    [layout.blockSize, scrollMapper],\n  );\n\n  const applyLogicalScrollTop = React.useCallback(\n    (\n      viewportElement: HTMLDivElement,\n      targetTop: number,\n      options: ScrollToOptions,\n      {\n        allowDefer,\n        preservePhysicalScrollTop = false,\n      }: { allowDefer: boolean; preservePhysicalScrollTop?: boolean },\n    ) => {\n      if (preservePhysicalScrollTop) {\n        scrollPageOffsetRef.current = targetTop - viewportElement.scrollTop;\n        cacheReadingAnchor({\n          scrollTop: targetTop,\n          viewportBlockSize: viewportElement.clientHeight,\n        });\n        cacheVisualLayerRect(viewportElement);\n        return true;\n      }\n\n      const { expectedMaxPhysicalScrollTop, position } =\n        resolveLogicalScrollPosition(viewportElement, targetTop);\n      const currentMaxPhysicalScrollTop =\n        getViewportMaxPhysicalScrollTop(viewportElement);\n      const shouldDefer =\n        allowDefer &&\n        position.physicalScrollTop >\n          currentMaxPhysicalScrollTop +\n            VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON &&\n        position.physicalScrollTop <=\n          expectedMaxPhysicalScrollTop +\n            VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON;\n\n      if (shouldDefer) {\n        schedulePendingScrollRestore({\n          attempts: 0,\n          options: {\n            ...options,\n            behavior: \"auto\",\n          },\n          targetTop,\n        });\n        return false;\n      }\n\n      scrollPageOffsetRef.current = position.scrollPageOffset;\n      markInternalScrollWrite();\n      scrollViewportToPhysicalTop(viewportElement, position.physicalScrollTop, {\n        behavior: \"auto\",\n        ...options,\n      });\n      return true;\n    },\n    [\n      cacheReadingAnchor,\n      cacheVisualLayerRect,\n      markInternalScrollWrite,\n      resolveLogicalScrollPosition,\n      schedulePendingScrollRestore,\n    ],\n  );\n\n  const flushPendingScrollRestore = React.useCallback(() => {\n    const pendingRestore = pendingScrollRestoreRef.current;\n    const viewportElement = viewportElementRef.current;\n    if (!pendingRestore || !viewportElement) return;\n\n    const { expectedMaxPhysicalScrollTop, position } =\n      resolveLogicalScrollPosition(viewportElement, pendingRestore.targetTop);\n    const currentMaxPhysicalScrollTop =\n      getViewportMaxPhysicalScrollTop(viewportElement);\n    const shouldWaitForRange =\n      position.physicalScrollTop >\n        currentMaxPhysicalScrollTop + VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON &&\n      position.physicalScrollTop <=\n        expectedMaxPhysicalScrollTop +\n          VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON &&\n      pendingRestore.attempts < VIEWER_DOCUMENT_SCROLL_RESTORE_MAX_FRAMES;\n\n    if (shouldWaitForRange) {\n      schedulePendingScrollRestore({\n        ...pendingRestore,\n        attempts: pendingRestore.attempts + 1,\n      });\n      return;\n    }\n\n    pendingScrollRestoreRef.current = null;\n    scrollPageOffsetRef.current = position.scrollPageOffset;\n    markInternalScrollWrite();\n    scrollViewportToPhysicalTop(\n      viewportElement,\n      position.physicalScrollTop,\n      pendingRestore.options,\n    );\n  }, [\n    markInternalScrollWrite,\n    resolveLogicalScrollPosition,\n    schedulePendingScrollRestore,\n  ]);\n\n  useKeyedLayoutEffect(joinEffectKey([flushPendingScrollRestore]), () => {\n    flushPendingScrollRestoreRef.current = flushPendingScrollRestore;\n    flushPendingScrollRestore();\n  });\n\n  const materializeScrollPageOffset = React.useCallback(() => {\n    const viewportElement = viewportElementRef.current;\n    if (\n      !viewportElement ||\n      !hasViewerDocumentScrollPageOffset(scrollPageOffsetRef.current)\n    ) {\n      return;\n    }\n\n    const metrics = readViewerDocumentScrollMetrics({\n      layout,\n      scrollMapper,\n      scrollPageOffset: scrollPageOffsetRef.current,\n      viewportElement,\n    });\n    const position = scrollMapper.resolvePhysicalScrollPosition({\n      blockSize: layout.blockSize,\n      logicalScrollTop: metrics.scrollTop,\n      scrollPageOffset: 0,\n      viewportBlockSize: metrics.viewportBlockSize,\n    });\n    scrollPageOffsetRef.current = position.scrollPageOffset;\n    markInternalScrollWrite();\n    scrollViewportToPhysicalTop(viewportElement, position.physicalScrollTop, {\n      behavior: \"auto\",\n    });\n    cacheReadingAnchor({\n      scrollTop: metrics.scrollTop,\n      viewportBlockSize: metrics.viewportBlockSize,\n    });\n    cacheVisualLayerRect(viewportElement);\n  }, [\n    cacheReadingAnchor,\n    cacheVisualLayerRect,\n    layout,\n    markInternalScrollWrite,\n    scrollMapper,\n  ]);\n\n  const scrollViewportToLogicalTop = React.useCallback(\n    (\n      viewportElement: HTMLDivElement,\n      targetTop: number,\n      options?: ScrollToOptions,\n      behavior?: {\n        preservePhysicalScrollTop?: boolean;\n      },\n    ) => {\n      applyLogicalScrollTop(\n        viewportElement,\n        targetTop,\n        {\n          behavior: \"auto\",\n          ...options,\n        },\n        {\n          allowDefer: true,\n          preservePhysicalScrollTop: behavior?.preservePhysicalScrollTop,\n        },\n      );\n      cacheReadingAnchor({\n        scrollTop: targetTop,\n        viewportBlockSize: viewportElement.clientHeight,\n      });\n    },\n    [applyLogicalScrollTop, cacheReadingAnchor],\n  );\n\n  const scrollViewportToResolvedTarget = React.useCallback(\n    (\n      viewportElement: HTMLDivElement,\n      target: ViewerDocumentResolvedScrollTarget,\n      options: ScrollToOptions,\n    ) => {\n      const physicalScrollSize = scrollMapper.getPhysicalScrollSize({\n        blockSize: layout.blockSize,\n        viewportBlockSize: viewportElement.clientHeight,\n      });\n      const position =\n        physicalScrollSize < layout.blockSize\n          ? scrollMapper.resolvePhysicalScrollPosition({\n              blockSize: layout.blockSize,\n              logicalScrollTop: target.top,\n              scrollPageOffset: scrollPageOffsetRef.current,\n              viewportBlockSize: viewportElement.clientHeight,\n            })\n          : {\n              physicalScrollTop: Math.max(0, target.top),\n              scrollPageOffset: 0,\n            };\n      scrollPageOffsetRef.current = position.scrollPageOffset;\n      viewportElement.scrollTo({\n        top: position.physicalScrollTop,\n        ...(target.left == null ? null : { left: target.left }),\n        ...options,\n      });\n    },\n    [layout.blockSize, scrollMapper],\n  );\n\n  useKeyedLayoutEffect(\n    joinEffectKey([\n      applyLogicalScrollTop,\n      copyScrollTarget,\n      cacheAnchor,\n      cacheReadingAnchor,\n      cacheVisualLayerRect,\n      cancelZoomMotion,\n      classifyLayoutChange,\n      commit,\n      layout,\n      markInternalScrollWrite,\n      prepare,\n      recordStableState,\n      resetKey,\n      resetGeometryTransaction,\n      resolveScrollTarget,\n      scrollIntent,\n      scrollMapper,\n      scrollViewportToLogicalTop,\n      scrollViewportToResolvedTarget,\n      syncViewportScrollPosition,\n      zoomMotion,\n    ]),\n    () => {\n      const previousLayout = committedLayoutRef.current;\n      const previousResetKey = committedResetKeyRef.current;\n      committedLayoutRef.current = layout;\n      committedResetKeyRef.current = resetKey;\n\n      const viewportElement = viewportElementRef.current;\n      if (!viewportElement) return;\n\n      const layoutChange = classifyLayoutChange({\n        previousLayout,\n        previousResetKey,\n      });\n      if (layoutChange === \"reset\") {\n        cancelZoomMotion();\n        resetGeometryTransaction(viewportElement);\n        return;\n      }\n\n      const currentLogicalScrollTop = scrollMapper.getLogicalScrollTop({\n        blockSize: layout.blockSize,\n        physicalScrollTop: viewportElement.scrollTop,\n        scrollPageOffset: scrollPageOffsetRef.current,\n        viewportBlockSize: viewportElement.clientHeight,\n      });\n      if (layoutChange !== \"geometry\") {\n        recordStableState({\n          clearTransition:\n            layoutChange === \"metadata\" && !layout.isTransitioning,\n          scrollTop: currentLogicalScrollTop,\n          viewportBlockSize: viewportElement.clientHeight,\n          viewportElement,\n        });\n        return;\n      }\n\n      // A fresh geometry commit owns the visual layer; an in-flight zoom\n      // relax against the previous geometry can no longer settle correctly.\n      const pendingZoomIntent = pendingZoomIntentRef.current;\n      cancelZoomMotion();\n\n      const transition = layout.transition;\n      if (pendingZoomIntent && zoomMotion) {\n        if (transition?.source === \"viewer-shell\") {\n          zoomMotion.noteBypass?.(\"shell-transition\");\n        } else if (\n          readViewerDocumentNow() - pendingZoomIntent.capturedAt >\n          VIEWER_DOCUMENT_ZOOM_INTENT_MAX_AGE_MS\n        ) {\n          zoomMotion.noteBypass?.(\"stale-intent\");\n        } else {\n          const zoomTarget = zoomMotion.resolveScrollTarget({\n            transaction: pendingZoomIntent.transaction,\n            viewportElement,\n          });\n          if (!zoomTarget) {\n            zoomMotion.noteBypass?.(\"resolve-failed\");\n          } else {\n            // Commit-then-relax: land the centered scroll inside this commit\n            // (never deferred), then relax the painted FLIP over it.\n            applyLogicalScrollTop(\n              viewportElement,\n              zoomTarget.top,\n              { behavior: \"auto\" },\n              { allowDefer: false },\n            );\n            if (zoomTarget.left != null) {\n              markInternalScrollWrite();\n              setViewportPhysicalScrollLeft(viewportElement, zoomTarget.left);\n            }\n            cacheReadingAnchor({\n              scrollTop: zoomTarget.top,\n              viewportBlockSize: viewportElement.clientHeight,\n            });\n            cacheVisualLayerRect(viewportElement);\n            activeZoomMotionCancelRef.current = zoomMotion.play({\n              transaction: pendingZoomIntent.transaction,\n              viewportElement,\n            });\n            return;\n          }\n        }\n      }\n\n      const activeIntent = scrollIntent.current();\n      if (\n        activeIntent.kind === \"programmatic\" &&\n        resolveScrollTarget &&\n        transition?.source !== \"viewer-shell\"\n      ) {\n        const metrics = syncViewportScrollPosition(viewportElement);\n        const target = resolveScrollTarget({\n          layout,\n          scrollTop: metrics.scrollTop,\n          target: activeIntent.target,\n          viewportElement,\n        });\n        if (!target) return;\n\n        const { intent, targetChanged } = scrollIntent.updateProgrammaticTarget(\n          activeIntent,\n          target,\n        );\n\n        if (targetChanged) {\n          scrollViewportToResolvedTarget(viewportElement, target, {\n            behavior: intent.behavior,\n          });\n          scrollIntent.scheduleProgrammaticCompletion(intent.sequence);\n        }\n        cacheVisualLayerRect(viewportElement);\n        return;\n      }\n\n      const previousLogicalScrollTop = scrollMapper.getLogicalScrollTop({\n        blockSize: previousLayout.blockSize,\n        physicalScrollTop: viewportElement.scrollTop,\n        scrollPageOffset: scrollPageOffsetRef.current,\n        viewportBlockSize: viewportElement.clientHeight,\n      });\n      const viewportBlockSize = viewportElement.clientHeight;\n      const transaction = prepare({\n        previousLayout,\n        scrollTop: previousLogicalScrollTop,\n        viewportBlockSize,\n      });\n      if (!transaction) return;\n\n      if (transaction.shouldRestoreScroll) {\n        const targetTop = layout.getReadingAnchorScrollTop({\n          anchor: transaction.anchor,\n          viewportBlockSize: transaction.viewportBlockSize,\n        });\n        if (targetTop != null) {\n          // Shell motions always rebase (commit-then-relax rebases scroll in\n          // the slide-start commit), so the restore is a real scroll write.\n          scrollViewportToLogicalTop(viewportElement, targetTop);\n        } else {\n          cacheAnchor({\n            anchor: transaction.anchor,\n            viewportBlockSize,\n          });\n        }\n      } else {\n        cacheAnchor({\n          anchor: transaction.anchor,\n          viewportBlockSize: transaction.viewportBlockSize,\n        });\n      }\n\n      commit({\n        transaction,\n        viewportElement,\n      });\n    },\n  );\n\n  const handleScroll = React.useCallback((): ViewerDocumentScrollEventResult => {\n    const viewportElement = viewportElementRef.current;\n    if (viewportElement && isInternalScrollWriteRef.current) {\n      const metrics = readScrollMetrics();\n      cacheReadingAnchor(metrics);\n      cacheVisualLayerRect(viewportElement);\n      return { source: \"internal\" };\n    }\n\n    if (viewportElement) {\n      syncViewportScrollPosition(viewportElement);\n    }\n\n    const activeIntent = scrollIntent.current();\n    if (activeIntent.kind === \"programmatic\") {\n      scrollIntent.scheduleProgrammaticCompletion(activeIntent.sequence);\n      return { source: \"programmatic\" };\n    } else {\n      scrollIntent.markUser();\n      return { source: \"user\" };\n    }\n  }, [\n    cacheReadingAnchor,\n    cacheVisualLayerRect,\n    readScrollMetrics,\n    scrollIntent,\n    syncViewportScrollPosition,\n  ]);\n\n  const markUserScrollIntent = React.useCallback(() => {\n    cancelZoomMotion();\n    materializeScrollPageOffset();\n    cancelPendingScrollRestore();\n    scrollIntent.markUser();\n  }, [\n    cancelPendingScrollRestore,\n    cancelZoomMotion,\n    materializeScrollPageOffset,\n    scrollIntent,\n  ]);\n\n  useKeyedMountEffect(joinEffectKey([resetKey, resetViewportForKey]), () => {\n    if (!didMountResetEffectRef.current) {\n      didMountResetEffectRef.current = true;\n      return;\n    }\n    const viewportElement = viewportElementRef.current;\n    if (viewportElement) {\n      resetViewportForKey(viewportElement, resetKey);\n    }\n  });\n\n  useKeyedMountEffect(\n    joinEffectKey([markUserScrollIntent, scrollIntent, viewportElement]),\n    () => {\n      const viewportElement = viewportElementRef.current;\n      if (!viewportElement) return;\n\n      const handleScrollEnd = () => {\n        const activeIntent = scrollIntent.current();\n        if (activeIntent.kind === \"programmatic\") {\n          scrollIntent.completeProgrammatic(activeIntent.sequence);\n        } else {\n          scrollIntent.reset();\n        }\n      };\n\n      viewportElement.addEventListener?.(\"wheel\", markUserScrollIntent, {\n        passive: true,\n      });\n      viewportElement.addEventListener?.(\"touchstart\", markUserScrollIntent, {\n        passive: true,\n      });\n      viewportElement.addEventListener?.(\"pointerdown\", markUserScrollIntent);\n      viewportElement.addEventListener?.(\"keydown\", markUserScrollIntent);\n      viewportElement.addEventListener?.(\"scrollend\", handleScrollEnd);\n\n      return () => {\n        viewportElement.removeEventListener?.(\"wheel\", markUserScrollIntent);\n        viewportElement.removeEventListener?.(\n          \"touchstart\",\n          markUserScrollIntent,\n        );\n        viewportElement.removeEventListener?.(\n          \"pointerdown\",\n          markUserScrollIntent,\n        );\n        viewportElement.removeEventListener?.(\"keydown\", markUserScrollIntent);\n        viewportElement.removeEventListener?.(\"scrollend\", handleScrollEnd);\n      };\n    },\n  );\n\n  const scrollToTarget = React.useCallback(\n    (target: Target, options?: ScrollToOptions) => {\n      const viewportElement = viewportElementRef.current;\n      const metrics = readScrollMetrics();\n      const resolvedTarget =\n        viewportElement && resolveScrollTarget\n          ? resolveScrollTarget({\n              layout,\n              scrollTop: metrics.scrollTop,\n              target,\n              viewportElement,\n            })\n          : null;\n      if (!viewportElement || !resolvedTarget) return;\n\n      cancelPendingScrollRestore();\n      cancelZoomMotion();\n      const behavior = options?.behavior ?? \"smooth\";\n      const intent = scrollIntent.startProgrammatic({\n        behavior,\n        copyScrollTarget,\n        resolvedTarget,\n        target,\n      });\n      scrollViewportToResolvedTarget(viewportElement, resolvedTarget, {\n        behavior: \"smooth\",\n        ...options,\n      });\n      scrollIntent.scheduleProgrammaticCompletion(intent.sequence);\n    },\n    [\n      cancelPendingScrollRestore,\n      cancelZoomMotion,\n      copyScrollTarget,\n      layout,\n      readScrollMetrics,\n      resolveScrollTarget,\n      scrollIntent,\n      scrollViewportToResolvedTarget,\n    ],\n  );\n  const getViewportElement = React.useCallback(\n    () => viewportElementRef.current,\n    [],\n  );\n\n  useMountEffect(() => () => {\n    clearInternalScrollWrite();\n    cancelPendingScrollRestore();\n    cancelZoomMotion();\n    cancelGeometryTransaction();\n    scrollIntent.clearProgrammaticTimeout();\n  });\n\n  return React.useMemo(\n    () => ({\n      captureZoomIntent,\n      getScrollMetrics: readScrollMetrics,\n      getViewportElement,\n      handleScroll,\n      scrollToTarget,\n      scrollViewportToLogicalTop,\n      setViewportElement,\n      syncScrollPosition,\n      viewportElement,\n    }),\n    [\n      captureZoomIntent,\n      getViewportElement,\n      handleScroll,\n      readScrollMetrics,\n      scrollToTarget,\n      scrollViewportToLogicalTop,\n      setViewportElement,\n      syncScrollPosition,\n      viewportElement,\n    ],\n  );\n}\n\nfunction useViewerDocumentScrollIntentController<Target>() {\n  const scrollIntentRef = React.useRef<ViewerDocumentScrollIntent<Target>>({\n    kind: \"idle\",\n  });\n  const scrollIntentSequenceRef = React.useRef(0);\n  const programmaticScrollIdleTimeoutRef = React.useRef<ReturnType<\n    typeof setTimeout\n  > | null>(null);\n\n  const clearProgrammaticTimeout = React.useCallback(() => {\n    if (programmaticScrollIdleTimeoutRef.current) {\n      clearTimeout(programmaticScrollIdleTimeoutRef.current);\n      programmaticScrollIdleTimeoutRef.current = null;\n    }\n  }, []);\n\n  const reset = React.useCallback(() => {\n    clearProgrammaticTimeout();\n    scrollIntentRef.current = { kind: \"idle\" };\n  }, [clearProgrammaticTimeout]);\n\n  const completeProgrammatic = React.useCallback(\n    (sequence: number) => {\n      if (\n        scrollIntentRef.current.kind === \"programmatic\" &&\n        scrollIntentRef.current.sequence === sequence\n      ) {\n        scrollIntentRef.current = { kind: \"idle\" };\n      }\n      clearProgrammaticTimeout();\n    },\n    [clearProgrammaticTimeout],\n  );\n\n  const scheduleProgrammaticCompletion = React.useCallback(\n    (sequence: number) => {\n      clearProgrammaticTimeout();\n      programmaticScrollIdleTimeoutRef.current = setTimeout(() => {\n        completeProgrammatic(sequence);\n      }, VIEWER_DOCUMENT_SCROLL_IDLE_MS);\n    },\n    [clearProgrammaticTimeout, completeProgrammatic],\n  );\n\n  const markUser = React.useCallback(() => {\n    clearProgrammaticTimeout();\n    scrollIntentRef.current = { kind: \"user\" };\n  }, [clearProgrammaticTimeout]);\n\n  const startProgrammatic = React.useCallback(\n    ({\n      behavior,\n      copyScrollTarget,\n      resolvedTarget,\n      target,\n    }: {\n      behavior: ScrollBehavior;\n      copyScrollTarget?: (target: Target) => Target;\n      resolvedTarget: ViewerDocumentResolvedScrollTarget;\n      target: Target;\n    }) => {\n      const sequence = scrollIntentSequenceRef.current + 1;\n      scrollIntentSequenceRef.current = sequence;\n      clearProgrammaticTimeout();\n\n      const intent: Extract<\n        ViewerDocumentScrollIntent<Target>,\n        { kind: \"programmatic\" }\n      > = {\n        behavior,\n        kind: \"programmatic\",\n        sequence,\n        target: copyScrollTarget ? copyScrollTarget(target) : target,\n        targetLeft: resolvedTarget.left,\n        targetTop: resolvedTarget.top,\n      };\n      scrollIntentRef.current = intent;\n      return intent;\n    },\n    [clearProgrammaticTimeout],\n  );\n\n  const updateProgrammaticTarget = React.useCallback(\n    (\n      intent: Extract<\n        ViewerDocumentScrollIntent<Target>,\n        { kind: \"programmatic\" }\n      >,\n      target: ViewerDocumentResolvedScrollTarget,\n    ) => {\n      const targetChanged =\n        Math.abs(intent.targetTop - target.top) >\n          VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON ||\n        Math.abs((intent.targetLeft ?? 0) - (target.left ?? 0)) >\n          VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON;\n      const nextIntent = {\n        ...intent,\n        targetLeft: target.left,\n        targetTop: target.top,\n      };\n      scrollIntentRef.current = nextIntent;\n      return { intent: nextIntent, targetChanged };\n    },\n    [],\n  );\n\n  useKeyedMountEffect(joinEffectKey([clearProgrammaticTimeout]), () => () => {\n    clearProgrammaticTimeout();\n  });\n\n  return React.useMemo(\n    () => ({\n      clearProgrammaticTimeout,\n      completeProgrammatic,\n      current: () => scrollIntentRef.current,\n      markUser,\n      reset,\n      scheduleProgrammaticCompletion,\n      startProgrammatic,\n      updateProgrammaticTarget,\n    }),\n    [\n      clearProgrammaticTimeout,\n      completeProgrammatic,\n      markUser,\n      reset,\n      scheduleProgrammaticCompletion,\n      startProgrammatic,\n      updateProgrammaticTarget,\n    ],\n  );\n}\n\nfunction readViewerDocumentScrollMetrics<Anchor>({\n  layout,\n  scrollMapper,\n  scrollPageOffset,\n  viewportElement,\n}: {\n  layout: ViewerDocumentLayoutModel<Anchor>;\n  scrollMapper: ViewerDocumentScrollMapper;\n  scrollPageOffset: number;\n  viewportElement: HTMLDivElement | null;\n}): ViewerDocumentScrollMetrics {\n  const viewportBlockSize = viewportElement?.clientHeight ?? 0;\n  const physicalScrollTop = viewportElement?.scrollTop ?? 0;\n  const physicalScrollSize = scrollMapper.getPhysicalScrollSize({\n    blockSize: layout.blockSize,\n    viewportBlockSize,\n  });\n  const isRebased = physicalScrollSize < layout.blockSize;\n  const hasLogicalOffset = hasViewerDocumentScrollPageOffset(scrollPageOffset);\n  const shouldUseLogicalOffset = isRebased || hasLogicalOffset;\n\n  return {\n    physicalScrollSize,\n    physicalScrollTop,\n    scrollPageOffset: shouldUseLogicalOffset ? scrollPageOffset : 0,\n    scrollTop: shouldUseLogicalOffset\n      ? scrollMapper.getLogicalScrollTop({\n          blockSize: layout.blockSize,\n          physicalScrollTop,\n          scrollPageOffset,\n          viewportBlockSize,\n        })\n      : Math.max(0, physicalScrollTop),\n    viewportBlockSize,\n  };\n}\n\nfunction hasViewerDocumentScrollPageOffset(scrollPageOffset: number) {\n  return Math.abs(scrollPageOffset) > VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON;\n}\n\nfunction scrollViewportToPhysicalTop(\n  viewportElement: HTMLDivElement,\n  targetTop: number,\n  options: ScrollToOptions,\n) {\n  if (\n    Math.abs(viewportElement.scrollTop - targetTop) <=\n    VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON\n  ) {\n    return;\n  }\n\n  if (typeof viewportElement.scrollTo === \"function\") {\n    viewportElement.scrollTo({\n      top: targetTop,\n      ...options,\n    });\n    return;\n  }\n\n  setViewportPhysicalScrollTop(viewportElement, targetTop);\n}\n\nfunction getViewportMaxPhysicalScrollTop(viewportElement: HTMLDivElement) {\n  return Math.max(\n    0,\n    viewportElement.scrollHeight - viewportElement.clientHeight,\n  );\n}\n\nfunction setViewportPhysicalScrollTop(\n  viewportElement: HTMLDivElement,\n  targetTop: number,\n) {\n  if (\n    Math.abs(viewportElement.scrollTop - targetTop) <=\n    VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON\n  ) {\n    return;\n  }\n\n  viewportElement.scrollTop = targetTop;\n}\n\n// Raw assignment on purpose: the browser clamps to the live scrollable range\n// (including RTL's negative coordinate space), which is exactly the clamp the\n// centered zoom wants at the document's inline edges.\nfunction setViewportPhysicalScrollLeft(\n  viewportElement: HTMLDivElement,\n  targetLeft: number,\n) {\n  if (\n    !Number.isFinite(targetLeft) ||\n    Math.abs(viewportElement.scrollLeft - targetLeft) <=\n      VIEWER_DOCUMENT_SCROLL_POSITION_EPSILON\n  ) {\n    return;\n  }\n\n  viewportElement.scrollLeft = targetLeft;\n}\n\nfunction readViewerDocumentNow() {\n  return typeof performance !== \"undefined\" &&\n    typeof performance.now === \"function\"\n    ? performance.now()\n    : Date.now();\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-document-scroll.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/viewer-document-geometry.ts",
      "content": "import * as React from \"react\";\n\nimport type {\n  ViewerDocumentLayoutModel,\n  ViewerDocumentTransition,\n} from \"./viewer-types\";\n\nconst VIEWER_DOCUMENT_FLIP_LAYER_SELECTOR = \"[data-viewer-document-flip-layer]\";\nconst VIEWER_DOCUMENT_FLIP_TRANSITION_MS = 150;\nconst VIEWER_DOCUMENT_FLIP_TRANSITION_CLEANUP_MS =\n  VIEWER_DOCUMENT_FLIP_TRANSITION_MS + 50;\nconst VIEWER_DOCUMENT_FLIP_EPSILON = 0.5;\nconst VIEWER_DOCUMENT_LAYOUT_EPSILON = 0.5;\n\ntype ViewerDocumentGeometryLayoutChange =\n  | \"geometry\"\n  | \"metadata\"\n  | \"reset\"\n  | \"same\";\n\ntype ViewerDocumentTransitionAnchor<Anchor> = {\n  anchor: Anchor;\n  transitionId: ViewerDocumentTransition[\"transitionId\"];\n  viewportBlockSize: number;\n};\n\ntype ViewerDocumentReadingAnchor<Anchor> = {\n  anchor: Anchor;\n  resetKey: unknown;\n  viewportBlockSize: number;\n};\n\ntype ViewerDocumentFlipAnimation = {\n  element: HTMLElement;\n  frame: number;\n  timeout: ReturnType<typeof setTimeout>;\n};\n\nexport type ViewerDocumentGeometryTransaction<Anchor> = {\n  anchor: Anchor;\n  previousVisualLayerRect: DOMRectReadOnly | null;\n  shouldAnimate: boolean;\n  shouldRestoreScroll: boolean;\n  viewportBlockSize: number;\n};\n\nexport function useViewerDocumentGeometryTransaction<Anchor>({\n  layout,\n  resetKey,\n}: {\n  layout: ViewerDocumentLayoutModel<Anchor>;\n  resetKey: unknown;\n}) {\n  const transitionAnchorRef =\n    React.useRef<ViewerDocumentTransitionAnchor<Anchor> | null>(null);\n  const readingAnchorRef =\n    React.useRef<ViewerDocumentReadingAnchor<Anchor> | null>(null);\n  const visualLayerRectRef = React.useRef<DOMRectReadOnly | null>(null);\n  const flipAnimationRef = React.useRef<ViewerDocumentFlipAnimation | null>(\n    null,\n  );\n\n  const cancel = React.useCallback(() => {\n    cancelViewerDocumentFlipAnimation(flipAnimationRef.current);\n    flipAnimationRef.current = null;\n  }, []);\n\n  const cacheVisualLayerRect = React.useCallback(\n    (viewportElement: HTMLDivElement) => {\n      if (flipAnimationRef.current) return;\n      visualLayerRectRef.current =\n        readViewerDocumentVisualLayerRect(viewportElement);\n    },\n    [],\n  );\n\n  const cacheReadingAnchor = React.useCallback(\n    ({\n      scrollTop,\n      viewportBlockSize,\n    }: {\n      scrollTop: number;\n      viewportBlockSize: number;\n    }) => {\n      const anchor = layout.captureReadingAnchor({\n        scrollTop,\n        viewportBlockSize,\n      });\n      readingAnchorRef.current = anchor\n        ? {\n            anchor,\n            resetKey,\n            viewportBlockSize,\n          }\n        : null;\n      return anchor;\n    },\n    [layout, resetKey],\n  );\n\n  const classifyLayoutChange = React.useCallback(\n    ({\n      previousLayout,\n      previousResetKey,\n    }: {\n      previousLayout: ViewerDocumentLayoutModel<Anchor>;\n      previousResetKey: unknown;\n    }): ViewerDocumentGeometryLayoutChange => {\n      if (!Object.is(previousResetKey, resetKey)) return \"reset\";\n      if (Object.is(previousLayout, layout)) return \"same\";\n      if (!hasViewerDocumentLayoutGeometryChange(previousLayout, layout)) {\n        return \"metadata\";\n      }\n      return \"geometry\";\n    },\n    [layout, resetKey],\n  );\n\n  const reset = React.useCallback(\n    (viewportElement: HTMLDivElement) => {\n      transitionAnchorRef.current = null;\n      readingAnchorRef.current = null;\n      cancel();\n      cacheVisualLayerRect(viewportElement);\n    },\n    [cacheVisualLayerRect, cancel],\n  );\n\n  const recordStableState = React.useCallback(\n    ({\n      clearTransition = false,\n      scrollTop,\n      viewportBlockSize,\n      viewportElement,\n    }: {\n      clearTransition?: boolean;\n      scrollTop: number;\n      viewportBlockSize: number;\n      viewportElement: HTMLDivElement;\n    }) => {\n      if (clearTransition) transitionAnchorRef.current = null;\n      const anchor = cacheReadingAnchor({ scrollTop, viewportBlockSize });\n      if (anchor && shouldRetainViewerDocumentTransitionAnchor(layout)) {\n        const transition = getViewerDocumentTransition(layout);\n        transitionAnchorRef.current = {\n          anchor,\n          transitionId: transition.transitionId,\n          viewportBlockSize,\n        };\n      }\n      cacheVisualLayerRect(viewportElement);\n    },\n    [cacheReadingAnchor, cacheVisualLayerRect, layout],\n  );\n\n  const prepare = React.useCallback(\n    ({\n      previousLayout,\n      scrollTop,\n      viewportBlockSize,\n    }: {\n      previousLayout: ViewerDocumentLayoutModel<Anchor>;\n      scrollTop: number;\n      viewportBlockSize: number;\n    }): ViewerDocumentGeometryTransaction<Anchor> | null => {\n      const isLayoutTransition = isViewerDocumentLayoutTransition({\n        layout,\n        previousLayout,\n      });\n      const transition = resolveViewerDocumentTransition({\n        layout,\n        previousLayout,\n      });\n      const previousTransitionAnchor =\n        getReusableViewerDocumentTransitionAnchor({\n          anchor: transitionAnchorRef.current,\n          transition,\n        });\n      const cachedReadingAnchor = Object.is(\n        readingAnchorRef.current?.resetKey,\n        resetKey,\n      )\n        ? readingAnchorRef.current\n        : null;\n      const shouldUseCachedShellTransitionAnchor =\n        isLayoutTransition &&\n        transition.source === \"viewer-shell\" &&\n        transition.layoutPolicy === \"target\" &&\n        Boolean(cachedReadingAnchor);\n      const shouldCaptureLiveShellTransitionAnchor =\n        isLayoutTransition &&\n        transition.source === \"viewer-shell\" &&\n        !previousTransitionAnchor &&\n        !shouldUseCachedShellTransitionAnchor;\n      const liveReadingAnchor = shouldCaptureLiveShellTransitionAnchor\n        ? previousLayout.captureReadingAnchor({\n            scrollTop,\n            viewportBlockSize,\n          })\n        : null;\n      const anchor =\n        isLayoutTransition && previousTransitionAnchor\n          ? previousTransitionAnchor.anchor\n          : liveReadingAnchor\n            ? liveReadingAnchor\n            : isLayoutTransition && cachedReadingAnchor\n              ? cachedReadingAnchor.anchor\n              : previousLayout.captureReadingAnchor({\n                  scrollTop,\n                  viewportBlockSize,\n                });\n      if (!anchor) return null;\n\n      const transactionViewportBlockSize =\n        previousTransitionAnchor?.viewportBlockSize ??\n        (liveReadingAnchor ? viewportBlockSize : null) ??\n        cachedReadingAnchor?.viewportBlockSize ??\n        viewportBlockSize;\n\n      if (shouldRetainViewerDocumentTransitionAnchor(layout)) {\n        transitionAnchorRef.current = {\n          anchor,\n          transitionId: transition.transitionId,\n          viewportBlockSize: transactionViewportBlockSize,\n        };\n      } else {\n        transitionAnchorRef.current = null;\n      }\n\n      return {\n        anchor,\n        previousVisualLayerRect: visualLayerRectRef.current,\n        shouldAnimate:\n          isLayoutTransition && transition.visualPolicy === \"document-flip\",\n        // Every remaining scroll policy restores (rebase) or preserves through\n        // an explicit restore target; nothing defers anymore.\n        shouldRestoreScroll: true,\n        viewportBlockSize: transactionViewportBlockSize,\n      };\n    },\n    [layout, resetKey],\n  );\n\n  const cacheAnchor = React.useCallback(\n    ({\n      anchor,\n      viewportBlockSize,\n    }: {\n      anchor: Anchor;\n      viewportBlockSize: number;\n    }) => {\n      readingAnchorRef.current = {\n        anchor,\n        resetKey,\n        viewportBlockSize,\n      };\n    },\n    [resetKey],\n  );\n\n  const commit = React.useCallback(\n    ({\n      transaction,\n      viewportElement,\n    }: {\n      transaction: ViewerDocumentGeometryTransaction<Anchor>;\n      viewportElement: HTMLDivElement;\n    }) => {\n      visualLayerRectRef.current = playViewerDocumentFlipAnimation({\n        animationRef: flipAnimationRef,\n        enabled: transaction.shouldAnimate,\n        previousRect: transaction.previousVisualLayerRect,\n        viewportElement,\n      });\n    },\n    [],\n  );\n\n  return React.useMemo(\n    () => ({\n      cacheAnchor,\n      cacheReadingAnchor,\n      cacheVisualLayerRect,\n      cancel,\n      classifyLayoutChange,\n      commit,\n      prepare,\n      recordStableState,\n      reset,\n    }),\n    [\n      cacheAnchor,\n      cacheReadingAnchor,\n      cacheVisualLayerRect,\n      cancel,\n      classifyLayoutChange,\n      commit,\n      prepare,\n      recordStableState,\n      reset,\n    ],\n  );\n}\n\nconst VIEWER_DOCUMENT_STABLE_TRANSITION: ViewerDocumentTransition = {\n  layoutPolicy: \"live\",\n  scrollPolicy: \"preserve\",\n  source: \"none\",\n  transitionId: null,\n  visualPolicy: \"none\",\n};\n\nfunction getViewerDocumentTransition<Anchor>(\n  layout: ViewerDocumentLayoutModel<Anchor>,\n) {\n  return layout.transition ?? VIEWER_DOCUMENT_STABLE_TRANSITION;\n}\n\nfunction isViewerDocumentLayoutTransition<Anchor>({\n  layout,\n  previousLayout,\n}: {\n  layout: ViewerDocumentLayoutModel<Anchor>;\n  previousLayout: ViewerDocumentLayoutModel<Anchor>;\n}) {\n  return (\n    previousLayout.isTransitioning === true ||\n    layout.isTransitioning === true ||\n    getViewerDocumentTransition(previousLayout).source !== \"none\" ||\n    getViewerDocumentTransition(layout).source !== \"none\"\n  );\n}\n\nfunction resolveViewerDocumentTransition<Anchor>({\n  layout,\n  previousLayout,\n}: {\n  layout: ViewerDocumentLayoutModel<Anchor>;\n  previousLayout: ViewerDocumentLayoutModel<Anchor>;\n}) {\n  const transition = getViewerDocumentTransition(layout);\n  if (transition.source !== \"none\") return transition;\n\n  const previousTransition = getViewerDocumentTransition(previousLayout);\n  if (previousTransition.source !== \"none\") return previousTransition;\n\n  if (\n    previousLayout.isTransitioning === true ||\n    layout.isTransitioning === true\n  ) {\n    return {\n      layoutPolicy: \"live\",\n      scrollPolicy: \"preserve\",\n      source: \"document-layout\",\n      transitionId: null,\n      visualPolicy: \"document-flip\",\n    } satisfies ViewerDocumentTransition;\n  }\n\n  return VIEWER_DOCUMENT_STABLE_TRANSITION;\n}\n\nfunction shouldRetainViewerDocumentTransitionAnchor<Anchor>(\n  layout: ViewerDocumentLayoutModel<Anchor>,\n) {\n  const transition = getViewerDocumentTransition(layout);\n  return layout.isTransitioning === true;\n}\n\nfunction getReusableViewerDocumentTransitionAnchor<Anchor>({\n  anchor,\n  transition,\n}: {\n  anchor: ViewerDocumentTransitionAnchor<Anchor> | null;\n  transition: ViewerDocumentTransition;\n}) {\n  if (!anchor) return null;\n  if (transition.source === \"viewer-shell\") {\n    return anchor.transitionId !== null &&\n      transition.transitionId !== null &&\n      Object.is(anchor.transitionId, transition.transitionId)\n      ? anchor\n      : null;\n  }\n  if (anchor.transitionId === null || transition.transitionId === null) {\n    return anchor;\n  }\n  return Object.is(anchor.transitionId, transition.transitionId)\n    ? anchor\n    : null;\n}\n\nfunction playViewerDocumentFlipAnimation({\n  animationRef,\n  enabled,\n  previousRect,\n  viewportElement,\n}: {\n  animationRef: React.MutableRefObject<ViewerDocumentFlipAnimation | null>;\n  enabled: boolean;\n  previousRect: DOMRectReadOnly | null;\n  viewportElement: HTMLDivElement;\n}) {\n  const visualLayer = findViewerDocumentVisualLayer(viewportElement);\n  const currentRect = readElementRect(visualLayer);\n  if (!currentRect) return null;\n\n  if (\n    !visualLayer ||\n    !enabled ||\n    !previousRect ||\n    typeof requestAnimationFrame !== \"function\"\n  ) {\n    return currentRect;\n  }\n\n  const transform = getViewerDocumentFlipTransform({\n    currentRect,\n    previousRect,\n  });\n  if (!transform) return currentRect;\n\n  cancelViewerDocumentFlipAnimation(animationRef.current);\n\n  visualLayer.style.transition = \"none\";\n  visualLayer.style.transformOrigin = \"top left\";\n  visualLayer.style.transform = transform;\n  visualLayer.style.willChange = \"transform\";\n\n  const frame = requestAnimationFrame(() => {\n    visualLayer.style.transition = `transform ${VIEWER_DOCUMENT_FLIP_TRANSITION_MS}ms linear`;\n    visualLayer.style.transform = \"translate3d(0px, 0px, 0px) scale(1, 1)\";\n  });\n  const timeout = setTimeout(() => {\n    if (animationRef.current?.element !== visualLayer) return;\n    finishViewerDocumentFlipAnimation(visualLayer);\n    animationRef.current = null;\n  }, VIEWER_DOCUMENT_FLIP_TRANSITION_CLEANUP_MS);\n\n  animationRef.current = {\n    element: visualLayer,\n    frame,\n    timeout,\n  };\n  return currentRect;\n}\n\nfunction cancelViewerDocumentFlipAnimation(\n  animation: ViewerDocumentFlipAnimation | null,\n) {\n  if (!animation) return;\n  if (typeof cancelAnimationFrame === \"function\") {\n    cancelAnimationFrame(animation.frame);\n  }\n  clearTimeout(animation.timeout);\n  finishViewerDocumentFlipAnimation(animation.element);\n}\n\nfunction finishViewerDocumentFlipAnimation(element: HTMLElement) {\n  element.style.transition = \"\";\n  element.style.transform = \"\";\n  element.style.transformOrigin = \"\";\n  element.style.willChange = \"\";\n}\n\nfunction getViewerDocumentFlipTransform({\n  currentRect,\n  previousRect,\n}: {\n  currentRect: DOMRectReadOnly;\n  previousRect: DOMRectReadOnly;\n}) {\n  if (\n    previousRect.width <= 0 ||\n    previousRect.height <= 0 ||\n    currentRect.width <= 0 ||\n    currentRect.height <= 0\n  ) {\n    return null;\n  }\n\n  const translateX = previousRect.left - currentRect.left;\n  const translateY = previousRect.top - currentRect.top;\n  const scaleX = previousRect.width / currentRect.width;\n  const scaleY = previousRect.height / currentRect.height;\n  const hasVisibleDelta =\n    Math.abs(translateX) > VIEWER_DOCUMENT_FLIP_EPSILON ||\n    Math.abs(translateY) > VIEWER_DOCUMENT_FLIP_EPSILON ||\n    Math.abs(1 - scaleX) > 0.001 ||\n    Math.abs(1 - scaleY) > 0.001;\n\n  if (!hasVisibleDelta) return null;\n\n  return `translate3d(${translateX}px, ${translateY}px, 0px) scale(${scaleX}, ${scaleY})`;\n}\n\nfunction hasViewerDocumentLayoutGeometryChange<Anchor>(\n  previousLayout: ViewerDocumentLayoutModel<Anchor>,\n  nextLayout: ViewerDocumentLayoutModel<Anchor>,\n) {\n  return (\n    !areViewerDocumentLayoutSizesEqual(\n      previousLayout.inlineSize,\n      nextLayout.inlineSize,\n    ) ||\n    !areViewerDocumentLayoutSizesEqual(\n      previousLayout.blockSize,\n      nextLayout.blockSize,\n    )\n  );\n}\n\nfunction areViewerDocumentLayoutSizesEqual(previous: number, next: number) {\n  return Math.abs(previous - next) < VIEWER_DOCUMENT_LAYOUT_EPSILON;\n}\n\nfunction readViewerDocumentVisualLayerRect(\n  viewportElement: HTMLDivElement | null,\n) {\n  return readElementRect(findViewerDocumentVisualLayer(viewportElement));\n}\n\nfunction findViewerDocumentVisualLayer(viewportElement: HTMLDivElement | null) {\n  if (!viewportElement || typeof viewportElement.querySelector !== \"function\") {\n    return null;\n  }\n\n  return viewportElement.querySelector<HTMLElement>(\n    VIEWER_DOCUMENT_FLIP_LAYER_SELECTOR,\n  );\n}\n\nfunction readElementRect(element: HTMLElement | null | undefined) {\n  if (!element || typeof element.getBoundingClientRect !== \"function\") {\n    return null;\n  }\n\n  const rect = element.getBoundingClientRect();\n  return rect.width > 0 && rect.height > 0 ? rect : null;\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-document-geometry.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/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"
}