import { RefObject, useEffect } from 'react'; interface UseWheelZoomOptions { /** * Element the wheel listener should be bound to. */ ref: RefObject; /** * Callback executed when the hook decides to zoom in. */ onZoomIn: () => void; /** * Callback executed when the hook decides to zoom out. */ onZoomOut: () => void; /** * Whether the wheel listener should be active. */ enabled?: boolean; /** * How much delta needs to accumulate before a zoom action is triggered. * Defaults to 10 which matches the previous implementations. */ threshold?: number; /** * Whether a Ctrl/Cmd modifier is required for zooming. Defaults to true so * we only react to pinch gestures and intentional ctrl+wheel zooming. */ requireModifierKey?: boolean; } /** * Shared hook for handling wheel-based zoom across components. * It normalises accumulated delta behaviour, prevents default scrolling when * zoom is triggered, and keeps the handler detached when disabled. */ export function useWheelZoom({ ref, onZoomIn, onZoomOut, enabled = true, threshold = 10, requireModifierKey = true, }: UseWheelZoomOptions) { useEffect(() => { if (!enabled) { return; } const element = ref.current; if (!element) { return; } let accumulator = 0; const handleWheel = (event: Event) => { const wheelEvent = event as WheelEvent; const hasModifier = wheelEvent.ctrlKey || wheelEvent.metaKey; if (requireModifierKey && !hasModifier) { return; } wheelEvent.preventDefault(); wheelEvent.stopPropagation(); accumulator += wheelEvent.deltaY; if (accumulator <= -threshold) { onZoomIn(); accumulator = 0; } else if (accumulator >= threshold) { onZoomOut(); accumulator = 0; } }; element.addEventListener('wheel', handleWheel, { passive: false }); return () => { element.removeEventListener('wheel', handleWheel); }; }, [ref, onZoomIn, onZoomOut, enabled, threshold, requireModifierKey]); }