{"version":3,"file":"useViewerGestures.mjs","names":[],"sources":["../../../src/Image/viewer/useViewerGestures.ts"],"sourcesContent":["import type { MouseEvent, PointerEvent } from 'react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport type { Point } from './geometry';\nimport type { WheelLikeEvent } from './useZoomPan';\n\nconst DRAG_THRESHOLD = 4;\nconst DOUBLE_CLICK_WINDOW = 250;\n\nexport type ViewerCursor = 'grab' | 'grabbing' | 'zoom-out';\n\nexport interface UseViewerGesturesOptions {\n  dragBy: (delta: Point) => void;\n  dragEnd: () => void;\n  handleDoubleClick: (point: Point) => void;\n  handleWheel: (event: WheelLikeEvent) => void;\n  isClean: boolean;\n  isZoomed: boolean;\n  onClose: () => void;\n  reset: () => void;\n  zoomIn: () => void;\n  zoomOut: () => void;\n}\n\nexport interface UseViewerGesturesResult {\n  cancelPendingClose: () => void;\n  cursor: ViewerCursor;\n  imageRef: (node: HTMLImageElement | null) => void;\n  onImageClick: (event: MouseEvent<HTMLElement>) => void;\n  onImageDoubleClick: (event: MouseEvent<HTMLElement>) => void;\n  onPointerDown: (event: PointerEvent<HTMLElement>) => void;\n  onPointerFinish: (event: PointerEvent<HTMLElement>) => void;\n  onPointerMove: (event: PointerEvent<HTMLElement>) => void;\n  onSurfaceClick: () => void;\n  popupRef: (node: HTMLElement | null) => void;\n}\n\ninterface DragState {\n  id: number;\n  last: Point;\n  panning: boolean;\n  start: Point;\n}\n\nconst capturePointer = (node: Element, pointerId: number) => {\n  if (typeof node.setPointerCapture === 'function') node.setPointerCapture(pointerId);\n};\n\nexport const isTypingTarget = (target: EventTarget | null) =>\n  target instanceof HTMLElement &&\n  (target.isContentEditable || ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName));\n\nexport const useViewerGestures = ({\n  dragBy,\n  dragEnd,\n  handleDoubleClick,\n  handleWheel,\n  isClean,\n  isZoomed,\n  onClose,\n  reset,\n  zoomIn,\n  zoomOut,\n}: UseViewerGesturesOptions): UseViewerGesturesResult => {\n  const [popup, setPopup] = useState<HTMLElement | null>(null);\n  const [panning, setPanning] = useState(false);\n\n  const imageNodeRef = useRef<HTMLImageElement | null>(null);\n  const dragRef = useRef<DragState | null>(null);\n  const movedRef = useRef(false);\n  const pendingCloseRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const isCleanRef = useRef(isClean);\n  isCleanRef.current = isClean;\n\n  const cancelPendingClose = useCallback(() => {\n    if (pendingCloseRef.current === null) return;\n    clearTimeout(pendingCloseRef.current);\n    pendingCloseRef.current = null;\n  }, []);\n\n  useEffect(() => cancelPendingClose, [cancelPendingClose]);\n\n  const imageRef = useCallback((node: HTMLImageElement | null) => {\n    imageNodeRef.current = node;\n  }, []);\n\n  useEffect(() => {\n    if (!popup) return;\n    const listener = (event: globalThis.WheelEvent) => handleWheel(event);\n    popup.addEventListener('wheel', listener, { passive: false });\n    return () => popup.removeEventListener('wheel', listener);\n  }, [handleWheel, popup]);\n\n  useEffect(() => {\n    const listener = (event: KeyboardEvent) => {\n      if (event.altKey || event.ctrlKey || event.metaKey || event.defaultPrevented) return;\n      if (isTypingTarget(event.target)) return;\n      switch (event.key) {\n        case '+':\n        case '=': {\n          zoomIn();\n          break;\n        }\n        case '-': {\n          zoomOut();\n          break;\n        }\n        case '0': {\n          reset();\n          break;\n        }\n        default: {\n          return;\n        }\n      }\n      event.preventDefault();\n    };\n    document.addEventListener('keydown', listener);\n    return () => document.removeEventListener('keydown', listener);\n  }, [reset, zoomIn, zoomOut]);\n\n  const onPointerDown = useCallback(\n    (event: PointerEvent<HTMLElement>) => {\n      if (dragRef.current?.panning) return;\n      if (event.button !== 0) return;\n      cancelPendingClose();\n      const start = { x: event.clientX, y: event.clientY };\n      const panEligible = isZoomed && event.target === imageNodeRef.current;\n      movedRef.current = false;\n      dragRef.current = { id: event.pointerId, last: start, panning: panEligible, start };\n      if (!panEligible) return;\n      capturePointer(event.target as Element, event.pointerId);\n      setPanning(true);\n    },\n    [cancelPendingClose, isZoomed],\n  );\n\n  const onPointerMove = useCallback(\n    (event: PointerEvent<HTMLElement>) => {\n      const drag = dragRef.current;\n      if (!drag || drag.id !== event.pointerId) return;\n      const point = { x: event.clientX, y: event.clientY };\n      if (Math.hypot(point.x - drag.start.x, point.y - drag.start.y) > DRAG_THRESHOLD) {\n        movedRef.current = true;\n      }\n      if (drag.panning) dragBy({ x: point.x - drag.last.x, y: point.y - drag.last.y });\n      drag.last = point;\n    },\n    [dragBy],\n  );\n\n  const onPointerFinish = useCallback(\n    (event: PointerEvent<HTMLElement>) => {\n      const drag = dragRef.current;\n      if (!drag || drag.id !== event.pointerId) return;\n      dragRef.current = null;\n      if (!drag.panning) return;\n      setPanning(false);\n      dragEnd();\n    },\n    [dragEnd],\n  );\n\n  const onSurfaceClick = useCallback(() => {\n    if (movedRef.current) return;\n    cancelPendingClose();\n    onClose();\n  }, [cancelPendingClose, onClose]);\n\n  const onImageClick = useCallback(\n    (event: MouseEvent<HTMLElement>) => {\n      event.stopPropagation();\n      cancelPendingClose();\n      // Gated on cleanliness rather than zoom: an image that *opened* zoomed\n      // (defaultZoom actual/auto) has been touched by nobody, so a plain click\n      // should still dismiss it, exactly as Esc does.\n      if (movedRef.current || !isCleanRef.current) return;\n      pendingCloseRef.current = setTimeout(() => {\n        pendingCloseRef.current = null;\n        if (!isCleanRef.current) return;\n        onClose();\n      }, DOUBLE_CLICK_WINDOW);\n    },\n    [cancelPendingClose, onClose],\n  );\n\n  const onImageDoubleClick = useCallback(\n    (event: MouseEvent<HTMLElement>) => {\n      event.stopPropagation();\n      cancelPendingClose();\n      handleDoubleClick({ x: event.clientX, y: event.clientY });\n    },\n    [cancelPendingClose, handleDoubleClick],\n  );\n\n  return {\n    cancelPendingClose,\n    cursor: isZoomed ? (panning ? 'grabbing' : 'grab') : 'zoom-out',\n    imageRef,\n    onImageClick,\n    onImageDoubleClick,\n    onPointerDown,\n    onPointerFinish,\n    onPointerMove,\n    onSurfaceClick,\n    popupRef: setPopup,\n  };\n};\n"],"mappings":"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"}