{"version":3,"file":"ImageViewer.mjs","names":[],"sources":["../../../src/Image/viewer/ImageViewer.tsx"],"sourcesContent":["'use client';\n\nimport { Dialog, type DialogRootProps } from '@base-ui/react/dialog';\nimport {\n  memo,\n  type SyntheticEvent,\n  use,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { useMergeRefs } from 'react-merge-refs';\n\nimport { ToastHost } from '@/base-ui/Toast';\nimport { useLayerZIndex } from '@/base-ui/zIndex';\nimport { MotionComponent } from '@/MotionProvider';\nimport { useAppElement } from '@/ThemeProvider';\n\nimport { styles } from '../style';\nimport { computeFit, type Size, unrotatedRect } from './geometry';\nimport { beginClosePreview, endClosePreview, type PreviewEntry } from './registry';\nimport { useFinalFocus } from './useFinalFocus';\nimport { useFlipTransition } from './useFlipTransition';\nimport { readNatural, useGalleryNav } from './useGalleryNav';\nimport { useRefitTransition } from './useRefitTransition';\nimport { useViewerGestures } from './useViewerGestures';\nimport { useZoomPan } from './useZoomPan';\nimport ViewerChrome from './ViewerChrome';\n\nexport interface ImageViewerProps {\n  entries: PreviewEntry[];\n  index: number;\n  openerFocusElement: HTMLElement | null;\n  token: number;\n}\n\nconst readViewport = (): Size => ({ height: window.innerHeight, width: window.innerWidth });\n\nconst prefersReducedMotion = () =>\n  window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n\nconst ImageViewer = memo<ImageViewerProps>(({ entries, index, openerFocusElement, token }) => {\n  const openerEntry = entries[index];\n  const appElement = useAppElement();\n  const motionComponent = use(MotionComponent);\n  const [animated] = useState(() => !!motionComponent && !prefersReducedMotion());\n\n  const { ref: layerRef, zIndex } = useLayerZIndex<HTMLDivElement>('modal');\n\n  const [currentIndex, setCurrentIndex] = useState(index);\n  const currentEntry = entries[currentIndex];\n  const currentEntryRef = useRef(currentEntry);\n  currentEntryRef.current = currentEntry;\n\n  const [source, setSource] = useState(openerEntry.src);\n  const [natural, setNatural] = useState<Size>(() => readNatural(openerEntry.element));\n  const [viewport, setViewport] = useState<Size>(readViewport);\n\n  const closeRef = useRef<(() => void) | null>(null);\n  const requestClose = useCallback(() => closeRef.current?.(), []);\n\n  // useZoomPan needs to gate its wheel/toolbar entry points on close-in-flight,\n  // but that state only exists once useFlipTransition (below) has run — and it\n  // needs useZoomPan's own transform values first. Break the cycle with a\n  // stable indirection: the ref is repointed at the real isClosing once it's\n  // available, written directly during render like the other *Ref fields here.\n  const isClosingIndirectRef = useRef<() => boolean>(() => false);\n  const isClosingIndirect = useCallback(() => isClosingIndirectRef.current(), []);\n\n  const isTransitioningIndirectRef = useRef<() => boolean>(() => true);\n  const isTransitioningIndirect = useCallback(() => isTransitioningIndirectRef.current(), []);\n\n  const fillViewport = Boolean(\n    currentEntry.previewSrc && currentEntry.previewSrc !== currentEntry.src,\n  );\n\n  const {\n    canZoomIn,\n    canZoomOut,\n    dragBy,\n    dragEnd,\n    escIntent,\n    flipHorizontal,\n    flipVertical,\n    flipX,\n    flipY,\n    getInitialScale,\n    handleDoubleClick,\n    handleWheel,\n    isClean,\n    isZoomed,\n    reset,\n    resetCloseTracking,\n    rotate,\n    rotateLeft,\n    rotateRight,\n    rotation,\n    scale,\n    setNatural: syncZoomNatural,\n    setViewport: syncZoomViewport,\n    toggleActualSize,\n    x,\n    y,\n    zoomIn,\n    zoomOut,\n  } = useZoomPan({\n    autoZoomThreshold: currentEntry.options.autoZoomThreshold,\n    defaultZoom: currentEntry.options.defaultZoom,\n    fillViewport,\n    isClosing: isClosingIndirect,\n    isTransitioning: isTransitioningIndirect,\n    maxScale: currentEntry.options.maxScale,\n    natural,\n    onCloseRequest: requestClose,\n    viewport,\n  });\n\n  const fitRect = useMemo(\n    () => computeFit(natural, viewport, rotation, fillViewport),\n    [natural, viewport, rotation, fillViewport],\n  );\n  const fitRectRef = useRef(fitRect);\n  fitRectRef.current = fitRect;\n  const getFitRect = useCallback(() => fitRectRef.current, []);\n\n  const imageRect = useMemo(() => unrotatedRect(fitRect, rotation), [fitRect, rotation]);\n\n  const viewportRef = useRef(viewport);\n  viewportRef.current = viewport;\n  const getViewportWidth = useCallback(() => viewportRef.current.width, []);\n\n  const getCloseSource = useCallback(() => currentEntryRef.current.element, []);\n\n  const transform = useMemo(\n    () => ({ flipX, flipY, rotate, scale, x, y }),\n    [flipX, flipY, rotate, scale, x, y],\n  );\n  const handleClosed = useCallback(() => endClosePreview(token), [token]);\n\n  const {\n    backdropRef,\n    chromeRef,\n    close,\n    imageRef: flipImageRef,\n    isClosing,\n    isTransitioning,\n    switchTo,\n  } = useFlipTransition({\n    animated,\n    getCloseSource,\n    getFitRect,\n    getInitialScale,\n    getViewportWidth,\n    onClosed: handleClosed,\n    source: openerEntry.element,\n    transform,\n  });\n  isClosingIndirectRef.current = isClosing;\n  isTransitioningIndirectRef.current = isTransitioning;\n\n  useRefitTransition({\n    animated,\n    fillViewport,\n    isTransitioning,\n    natural,\n    rotation,\n    scale,\n    viewport,\n    x,\n    y,\n  });\n  const finalFocus = useFinalFocus(openerFocusElement);\n\n  const isCleanRef = useRef(isClean);\n  isCleanRef.current = isClean;\n\n  const handleClose = useCallback(() => {\n    beginClosePreview(token);\n    close({ fade: !isCleanRef.current });\n  }, [close, token]);\n  closeRef.current = handleClose;\n\n  const handleDialogOpenChange = useCallback<NonNullable<DialogRootProps['onOpenChange']>>(\n    (open, eventDetails) => {\n      if (open) return;\n      // Base UI fires this on every Esc regardless of whether we're already\n      // mid-close (our Dialog.Root's open prop never actually flips to\n      // false), so without this guard a second Esc during the close window\n      // reads escIntent() off the close spring's mid-flight scale, decides\n      // 'reset', and cancels the close animation out from under itself.\n      if (isClosing()) return;\n      if (eventDetails.reason === 'escape-key' && escIntent() === 'reset') {\n        eventDetails.cancel();\n        reset();\n        return;\n      }\n      handleClose();\n    },\n    [escIntent, handleClose, isClosing, reset],\n  );\n\n  const gestures = useViewerGestures({\n    dragBy,\n    dragEnd,\n    handleDoubleClick,\n    handleWheel,\n    isClean,\n    isZoomed,\n    onClose: handleClose,\n    reset,\n    zoomIn,\n    zoomOut,\n  });\n\n  const { hasNext, hasPrev, next, prev } = useGalleryNav({\n    cancelPendingClose: gestures.cancelPendingClose,\n    currentIndex,\n    entries,\n    flipX,\n    flipY,\n    getInitialScale,\n    resetCloseTracking,\n    rotate,\n    scale,\n    setCurrentIndex,\n    setNatural,\n    setSource,\n    switchTo,\n    syncZoomNatural,\n    x,\n    y,\n  });\n\n  const popupNodeRef = useRef<HTMLDivElement | null>(null);\n  const popupRef = useMergeRefs<HTMLDivElement>([layerRef, gestures.popupRef, popupNodeRef]);\n  const imageRef = useMergeRefs<HTMLImageElement>([flipImageRef, gestures.imageRef]);\n\n  const handleLoad = useCallback(\n    (event: SyntheticEvent<HTMLImageElement>) => {\n      const loaded = event.currentTarget;\n      if (!loaded.naturalWidth || !loaded.naturalHeight) return;\n      const next = { height: loaded.naturalHeight, width: loaded.naturalWidth };\n      setNatural((prev) =>\n        prev.width === next.width && prev.height === next.height ? prev : next,\n      );\n      syncZoomNatural(next);\n    },\n    [syncZoomNatural],\n  );\n\n  useEffect(() => {\n    const handleResize = () => {\n      const next = readViewport();\n      setViewport(next);\n      syncZoomViewport(next);\n    };\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, [syncZoomViewport]);\n\n  useEffect(() => {\n    const { previewSrc, src } = currentEntry;\n    if (!previewSrc || previewSrc === src) return;\n    let cancelled = false;\n    const loader = new window.Image();\n    const handleLoaded = () => {\n      if (!cancelled) setSource(previewSrc);\n    };\n    loader.addEventListener('load', handleLoaded);\n    loader.src = previewSrc;\n    return () => {\n      cancelled = true;\n      loader.removeEventListener('load', handleLoaded);\n    };\n  }, [currentEntry]);\n\n  useEffect(\n    () => () => {\n      const thumbnailWasRemoved = !openerEntry.element.isConnected;\n      if (thumbnailWasRemoved) endClosePreview(token);\n    },\n    [openerEntry, token],\n  );\n\n  return (\n    <Dialog.Root modal open onOpenChange={handleDialogOpenChange}>\n      <Dialog.Portal container={appElement ?? undefined}>\n        {/* Base UI drops a nested dialog's backdrop so overlays don't stack. The viewer is\n            not an overlay on the surface it opened from — it replaces the screen — so it\n            keeps its own scrim, or a viewer opened from a maskless drawer would show the\n            page straight through the letterbox around the image. */}\n        <Dialog.Backdrop\n          forceRender\n          className={styles.viewerBackdrop}\n          ref={backdropRef}\n          style={zIndex === undefined ? undefined : { zIndex }}\n          onClick={gestures.onSurfaceClick}\n        />\n        <Dialog.Popup\n          aria-label={currentEntry.element.alt || undefined}\n          className={styles.viewerPopup}\n          finalFocus={finalFocus}\n          // Focus the popup itself instead of Base UI's default (the first\n          // tabbable — the close button): a focus-visible close button pins\n          // the idle auto-hide open forever and pops its tooltip on open.\n          initialFocus={popupNodeRef}\n          ref={popupRef}\n          style={zIndex === undefined ? undefined : { zIndex: zIndex + 1 }}\n          tabIndex={-1}\n          onClick={gestures.onSurfaceClick}\n          onPointerCancel={gestures.onPointerFinish}\n          onPointerDown={gestures.onPointerDown}\n          onPointerMove={gestures.onPointerMove}\n          onPointerUp={gestures.onPointerFinish}\n        >\n          <img\n            alt={currentEntry.element.alt}\n            className={styles.viewerImage}\n            ref={imageRef}\n            src={source}\n            style={{\n              cursor: gestures.cursor,\n              height: imageRect.height,\n              left: imageRect.x,\n              top: imageRect.y,\n              width: imageRect.width,\n            }}\n            onClick={gestures.onImageClick}\n            onDoubleClick={gestures.onImageDoubleClick}\n            onLoad={handleLoad}\n          />\n          <ViewerChrome\n            canZoomIn={canZoomIn}\n            canZoomOut={canZoomOut}\n            chromeRef={chromeRef}\n            current={currentIndex}\n            fitRect={fitRect}\n            flipHorizontal={flipHorizontal}\n            flipVertical={flipVertical}\n            hasNext={hasNext}\n            hasPrev={hasPrev}\n            natural={natural}\n            next={next}\n            prev={prev}\n            rotateLeft={rotateLeft}\n            rotateRight={rotateRight}\n            rotation={rotation}\n            scale={scale}\n            source={source}\n            toggleActualSize={toggleActualSize}\n            toolbarAddon={currentEntry.options.toolbarAddon}\n            total={entries.length}\n            zoomIn={zoomIn}\n            zoomOut={zoomOut}\n            onClose={handleClose}\n          />\n        </Dialog.Popup>\n        <ToastHost root={appElement ?? undefined} />\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n});\n\nImageViewer.displayName = 'ImageViewer';\n\nexport default ImageViewer;\n"],"mappings":"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"}