"use client"; import React from 'react'; type SetPos = React.Dispatch>; export interface DraggableOptions { constrainToViewport?: boolean; disableIframeDuringDrag?: boolean; } export function useDraggableModal( modalRef: React.RefObject, setModalPosition: SetPos, options: DraggableOptions = { constrainToViewport: true, disableIframeDuringDrag: true } ) { const dragState = React.useRef<{ dragging: boolean; startX: number; startY: number; origLeft: number; origTop: number; dx: number; dy: number; width: number; height: number; raf: number | null; iframe: HTMLIFrameElement | null; }>({ dragging: false, startX: 0, startY: 0, origLeft: 0, origTop: 0, dx: 0, dy: 0, width: 0, height: 0, raf: null, iframe: null }); const pointerDown = (e: React.PointerEvent) => { if (!modalRef.current) return; const rect = modalRef.current.getBoundingClientRect(); dragState.current.dragging = true; dragState.current.startX = e.clientX; dragState.current.startY = e.clientY; dragState.current.origLeft = rect.left; dragState.current.origTop = rect.top; dragState.current.dx = 0; dragState.current.dy = 0; dragState.current.width = rect.width; dragState.current.height = rect.height; // Improve paint performance modalRef.current.style.willChange = 'transform'; // Ensure no conflicting transform animation modalRef.current.style.animation = 'none'; // Optionally disable iframe interactions during drag if (options.disableIframeDuringDrag) { const iframe = modalRef.current.querySelector('iframe'); if (iframe) { dragState.current.iframe = iframe as HTMLIFrameElement; dragState.current.iframe.style.pointerEvents = 'none'; } else { dragState.current.iframe = null; } } (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }; const pointerMove = (e: React.PointerEvent) => { if (!dragState.current.dragging || !modalRef.current) return; // Raw delta let dx = e.clientX - dragState.current.startX; let dy = e.clientY - dragState.current.startY; if (options.constrainToViewport) { const maxLeft = Math.max(0, window.innerWidth - dragState.current.width); const maxTop = Math.max(0, window.innerHeight - dragState.current.height); const wantedLeft = dragState.current.origLeft + dx; const wantedTop = dragState.current.origTop + dy; const clampedLeft = Math.min(Math.max(0, wantedLeft), maxLeft); const clampedTop = Math.min(Math.max(0, wantedTop), maxTop); dx = clampedLeft - dragState.current.origLeft; dy = clampedTop - dragState.current.origTop; } dragState.current.dx = dx; dragState.current.dy = dy; if (dragState.current.raf) cancelAnimationFrame(dragState.current.raf); dragState.current.raf = requestAnimationFrame(() => { if (!modalRef.current) return; modalRef.current.style.transform = `translate3d(${dx}px, ${dy}px, 0)`; }); }; const pointerUp = (e: React.PointerEvent) => { if (!dragState.current.dragging) return; dragState.current.dragging = false; // Snap to the exact visual rect before clearing transform if (modalRef.current) { const rectNow = modalRef.current.getBoundingClientRect(); modalRef.current.style.left = `${rectNow.left}px`; modalRef.current.style.top = `${rectNow.top}px`; modalRef.current.style.right = 'auto'; modalRef.current.style.bottom = 'auto'; setModalPosition({ x: rectNow.left, y: rectNow.top }); } if (dragState.current.raf) { cancelAnimationFrame(dragState.current.raf); dragState.current.raf = null; } if (modalRef.current) { modalRef.current.style.transform = ''; modalRef.current.style.willChange = ''; } if (dragState.current.iframe) { dragState.current.iframe.style.pointerEvents = ''; dragState.current.iframe = null; } (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); }; return { onPointerDown: pointerDown, onPointerMove: pointerMove, onPointerUp: pointerUp, } as const; }