{"version":3,"file":"useGestures.mjs","names":[],"sources":["../../../../package/common/calendar/useGestures.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, type RefObject } from 'react';\n\nimport {\n  CLICK_SUPPRESS_MS,\n  edgeSpeed,\n  LOCK_DISTANCE,\n  LONG_PRESS_MS,\n  MOUSE_DRAG_DISTANCE,\n  RECENT_SCROLL_MS,\n  velocityOf,\n  type VelocitySample,\n} from './gesture';\n\nexport interface GesturePoint {\n  x: number;\n  y: number;\n}\n\nexport interface GestureHandlers<D> {\n  /** Every press that starts a gesture, before anything else: where a running settle is finished. */\n  onPress?: () => void;\n  /** Whether a horizontal swipe may page right now. */\n  canSwipe?: () => boolean;\n  onSwipeMove?: (dx: number) => void;\n  /** Also called with `(0, 0)` when the browser takes the gesture over. */\n  onSwipeEnd?: (dx: number, velocity: number) => void;\n  /** What a press on `target` would pick up, or `null` where nothing can be dragged. */\n  dragFrom?: (target: Element, point: GesturePoint) => D | null;\n  onDragStart?: (drag: D, point: GesturePoint) => void;\n  onDragMove?: (point: GesturePoint) => void;\n  onDragEnd?: (point: GesturePoint) => void;\n  onDragCancel?: () => void;\n  onPinchStart?: (center: GesturePoint, distance: number) => void;\n  onPinchMove?: (center: GesturePoint, distance: number) => void;\n  onPinchEnd?: () => void;\n  /** The element a drag scrolls near its edges, and the band of the viewport that counts as its inside. */\n  autoScroll?: () => { element: HTMLElement; top: number; bottom: number } | null;\n}\n\ntype Phase = 'idle' | 'pressed' | 'swiping' | 'dragging' | 'pinching' | 'passive';\n\n/**\n * One owner for every pointer gesture inside `ref`, so a swipe, a long-press drag, a pinch and\n * the browser's own scrolling can never both act on the same touch.\n *\n * Mouse or touch rules are chosen per event from `pointerType`, not once from a media query,\n * which answers wrongly on a laptop with a touch screen.\n */\nexport function useGestures<D>(ref: RefObject<HTMLElement | null>, handlers: GestureHandlers<D>): void {\n  const handlersRef = useRef(handlers);\n  useLayoutEffect(() => {\n    handlersRef.current = handlers;\n  });\n\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    const h = () => handlersRef.current;\n\n    let phase: Phase = 'idle';\n    let primary = -1;\n    let pointerType = '';\n    let origin: GesturePoint = { x: 0, y: 0 };\n    let last: GesturePoint = { x: 0, y: 0 };\n    let samples: VelocitySample[] = [];\n    let drag: D | null = null;\n    let timer: ReturnType<typeof setTimeout> | undefined;\n    let frame = 0;\n    let lastScroll = -Infinity;\n    let suppressUntil = 0;\n    const pointers = new Map<number, GesturePoint>();\n\n    function capture(pointerId: number) {\n      // Only once the gesture is ours: a captured pointer sends its click to the capture target,\n      // so capturing on press would swallow every click meant for an event inside the grid.\n      try {\n        root!.setPointerCapture(pointerId);\n      } catch {\n        // The pointer is already gone.\n      }\n    }\n\n    function clearTimer() {\n      clearTimeout(timer);\n      timer = undefined;\n    }\n\n    function finishGesture() {\n      clearTimer();\n      cancelAnimationFrame(frame);\n      frame = 0;\n      drag = null;\n      phase = 'passive';\n      suppressUntil = performance.now() + CLICK_SUPPRESS_MS;\n    }\n\n    function pinchGeometry() {\n      const [a, b] = [...pointers.values()];\n      return {\n        center: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },\n        distance: Math.hypot(a.x - b.x, a.y - b.y),\n      };\n    }\n\n    function scrollStep() {\n      if (phase !== 'dragging') return;\n      const area = h().autoScroll?.();\n      if (area) {\n        const speed = edgeSpeed(last.y, area.top, area.bottom);\n        const before = area.element.scrollTop;\n        if (speed !== 0) area.element.scrollTop += speed;\n        // The content moved under a finger that did not, so the drag target did.\n        if (area.element.scrollTop !== before) h().onDragMove?.(last);\n      }\n      frame = requestAnimationFrame(scrollStep);\n    }\n\n    function startDrag() {\n      timer = undefined;\n      if (drag === null || phase !== 'pressed') return;\n      phase = 'dragging';\n      capture(primary);\n      if (pointerType !== 'mouse' && 'vibrate' in navigator) navigator.vibrate(10);\n      h().onDragStart?.(drag, last);\n      frame = requestAnimationFrame(scrollStep);\n    }\n\n    function onPointerDown(event: PointerEvent) {\n      // A primary pointer means every earlier one is up, even where its pointerup never reached\n      // us (a mouse released outside the window before the drag captured it).\n      if (event.isPrimary && phase !== 'idle') {\n        if (phase === 'dragging') h().onDragCancel?.();\n        else if (phase === 'swiping') h().onSwipeEnd?.(0, 0);\n        else if (phase === 'pinching') h().onPinchEnd?.();\n        clearTimer();\n        cancelAnimationFrame(frame);\n        drag = null;\n        phase = 'idle';\n      }\n      if (phase === 'idle') {\n        if (event.pointerType === 'mouse' && event.button !== 0) return;\n        h().onPress?.();\n        primary = event.pointerId;\n        pointerType = event.pointerType;\n        origin = last = { x: event.clientX, y: event.clientY };\n        pointers.clear();\n        pointers.set(event.pointerId, origin);\n        samples = [{ time: event.timeStamp, x: event.clientX }];\n        drag = event.target instanceof Element ? (h().dragFrom?.(event.target, origin) ?? null) : null;\n        phase = 'pressed';\n        const stoppingMomentum = performance.now() - lastScroll < RECENT_SCROLL_MS;\n        if (drag !== null && pointerType !== 'mouse' && !stoppingMomentum) {\n          timer = setTimeout(startDrag, LONG_PRESS_MS);\n        }\n        return;\n      }\n      if (event.pointerType === 'mouse') return;\n      pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });\n      if (phase === 'pressed' && pointers.size === 2 && h().onPinchStart) {\n        clearTimer();\n        phase = 'pinching';\n        capture(primary);\n        capture(event.pointerId);\n        const { center, distance } = pinchGeometry();\n        h().onPinchStart?.(center, distance);\n      }\n    }\n\n    function onPointerMove(event: PointerEvent) {\n      if (!pointers.has(event.pointerId)) return;\n      const point = { x: event.clientX, y: event.clientY };\n      pointers.set(event.pointerId, point);\n\n      if (phase === 'pinching') {\n        if (pointers.size < 2) return;\n        const { center, distance } = pinchGeometry();\n        h().onPinchMove?.(center, distance);\n        return;\n      }\n      if (event.pointerId !== primary) return;\n      last = point;\n      const dx = point.x - origin.x;\n      const dy = point.y - origin.y;\n\n      if (phase === 'pressed') {\n        const distance = Math.hypot(dx, dy);\n        if (pointerType === 'mouse') {\n          if (distance <= MOUSE_DRAG_DISTANCE) return;\n          if (drag === null) {\n            phase = 'passive';\n            return;\n          }\n          startDrag();\n        } else {\n          if (distance <= LOCK_DISTANCE) return;\n          clearTimer();\n          if (Math.abs(dx) <= Math.abs(dy) || !h().canSwipe?.()) {\n            phase = 'passive';\n            return;\n          }\n          phase = 'swiping';\n          capture(primary);\n        }\n      }\n\n      if (phase === 'swiping') {\n        samples.push({ time: event.timeStamp, x: point.x });\n        if (samples.length > 20) samples.shift();\n        h().onSwipeMove?.(dx);\n      } else if (phase === 'dragging') {\n        h().onDragMove?.(point);\n      }\n    }\n\n    function release(pointerId: number) {\n      pointers.delete(pointerId);\n      if (pointers.size === 0) {\n        clearTimer();\n        drag = null;\n        phase = 'idle';\n      }\n    }\n\n    function onPointerUp(event: PointerEvent) {\n      if (!pointers.has(event.pointerId)) return;\n      if (phase === 'pinching') {\n        h().onPinchEnd?.();\n        finishGesture();\n      } else if (event.pointerId === primary) {\n        if (phase === 'swiping') {\n          samples.push({ time: event.timeStamp, x: event.clientX });\n          h().onSwipeEnd?.(event.clientX - origin.x, velocityOf(samples, event.timeStamp));\n          finishGesture();\n        } else if (phase === 'dragging') {\n          last = { x: event.clientX, y: event.clientY };\n          h().onDragEnd?.(last);\n          finishGesture();\n        } else if (phase === 'pressed') {\n          // Left to the click that follows, so a tap, a mouse click and Enter share one path.\n          clearTimer();\n          phase = 'passive';\n        }\n      }\n      release(event.pointerId);\n    }\n\n    // The browser took the touch over, usually to scroll: whatever was in progress is undone.\n    function onPointerCancel(event: PointerEvent) {\n      if (!pointers.has(event.pointerId)) return;\n      if (phase === 'swiping') {\n        h().onSwipeEnd?.(0, 0);\n        finishGesture();\n      } else if (phase === 'dragging') {\n        h().onDragCancel?.();\n        finishGesture();\n      } else if (phase === 'pinching') {\n        h().onPinchEnd?.();\n        finishGesture();\n      } else {\n        clearTimer();\n        phase = 'passive';\n      }\n      release(event.pointerId);\n    }\n\n    // The only way to keep iOS Safari from scrolling under a drag that has already begun:\n    // `touch-action` is read once, when the touch starts, so changing it mid-gesture does\n    // nothing. Registered here at mount rather than on touchstart, so the area is already known\n    // to block scrolling before the first touch arrives.\n    function onTouchMove(event: TouchEvent) {\n      if ((phase === 'swiping' || phase === 'dragging' || phase === 'pinching') && event.cancelable) {\n        event.preventDefault();\n      }\n    }\n\n    // Android opens the context menu on a long press, iOS the callout, both around the time a\n    // drag arms.\n    function onContextMenu(event: Event) {\n      if (phase === 'dragging' || (phase === 'pressed' && pointerType !== 'mouse' && drag !== null)) {\n        event.preventDefault();\n      }\n    }\n\n    function onDragStartNative(event: Event) {\n      event.preventDefault();\n    }\n\n    function onClickCapture(event: MouseEvent) {\n      if (performance.now() < suppressUntil) {\n        event.stopPropagation();\n        event.preventDefault();\n        suppressUntil = 0;\n      }\n    }\n\n    function onScroll() {\n      lastScroll = performance.now();\n    }\n\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key !== 'Escape' || phase !== 'dragging') return;\n      event.preventDefault();\n      h().onDragCancel?.();\n      finishGesture();\n    }\n\n    root.addEventListener('pointerdown', onPointerDown);\n    root.addEventListener('pointermove', onPointerMove);\n    root.addEventListener('pointerup', onPointerUp);\n    root.addEventListener('pointercancel', onPointerCancel);\n    root.addEventListener('touchmove', onTouchMove, { passive: false });\n    root.addEventListener('contextmenu', onContextMenu);\n    root.addEventListener('dragstart', onDragStartNative);\n    root.addEventListener('click', onClickCapture, true);\n    // `scroll` does not bubble, but it does pass through capturing listeners on the way down.\n    root.addEventListener('scroll', onScroll, { capture: true, passive: true });\n    window.addEventListener('scroll', onScroll, { passive: true });\n    window.addEventListener('keydown', onKeyDown);\n    return () => {\n      clearTimer();\n      cancelAnimationFrame(frame);\n      root.removeEventListener('pointerdown', onPointerDown);\n      root.removeEventListener('pointermove', onPointerMove);\n      root.removeEventListener('pointerup', onPointerUp);\n      root.removeEventListener('pointercancel', onPointerCancel);\n      root.removeEventListener('touchmove', onTouchMove);\n      root.removeEventListener('contextmenu', onContextMenu);\n      root.removeEventListener('dragstart', onDragStartNative);\n      root.removeEventListener('click', onClickCapture, true);\n      root.removeEventListener('scroll', onScroll, { capture: true });\n      window.removeEventListener('scroll', onScroll);\n      window.removeEventListener('keydown', onKeyDown);\n    };\n  }, [ref]);\n}\n"],"mappings":"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"}