import { useState, useRef, useCallback, useEffect, type RefObject } from 'react'; import { bindWindowDrag, clientXYFromReact, isPrimaryPointerButton } from '../utils/pointerDrag'; export interface DraggablePosition { x: number; y: number; } export interface UseDraggablePanelOptions { /** 마운트 후 패널 크기 기준 화면 중앙 배치 */ centerOnMount?: boolean; initialPosition?: DraggablePosition; panelRef?: RefObject; } const DEFAULT_POS: DraggablePosition = { x: 80, y: 72 }; const DEFAULT_CENTER_ESTIMATE = { width: 520, height: 560 }; function estimateCenterPosition( width = DEFAULT_CENTER_ESTIMATE.width, height = DEFAULT_CENTER_ESTIMATE.height, ): DraggablePosition { return { x: Math.max(8, (window.innerWidth - width) / 2), y: Math.max(8, (window.innerHeight - height) / 2), }; } /** * 팝업 패널 타이틀바 드래그 이동 (마우스·터치·펜) */ export function useDraggablePanel(options: UseDraggablePanelOptions = {}) { const { centerOnMount = false, initialPosition = DEFAULT_POS, panelRef: externalRef, } = options; const internalRef = useRef(null); const panelRef = (externalRef ?? internalRef) as RefObject; const [pos, setPos] = useState(() => centerOnMount ? estimateCenterPosition() : initialPosition, ); const [positionReady, setPositionReady] = useState(!centerOnMount); const [dragging, setDragging] = useState(false); const dragOrigin = useRef({ mx: 0, my: 0, px: 0, py: 0 }); const centeredOnce = useRef(false); const unbindDragRef = useRef<(() => void) | null>(null); useEffect(() => { if (!centerOnMount || centeredOnce.current) return; const el = panelRef.current; if (!el) return; const placeCenter = () => { const w = el.offsetWidth || DEFAULT_CENTER_ESTIMATE.width; const h = el.offsetHeight || DEFAULT_CENTER_ESTIMATE.height; setPos({ x: Math.max(8, (window.innerWidth - w) / 2), y: Math.max(8, (window.innerHeight - h) / 2), }); centeredOnce.current = true; setPositionReady(true); }; placeCenter(); const id = requestAnimationFrame(placeCenter); return () => cancelAnimationFrame(id); }, [centerOnMount, panelRef]); const endDrag = useCallback(() => { unbindDragRef.current?.(); unbindDragRef.current = null; setDragging(false); }, []); const onHeaderPointerDown = useCallback((e: React.PointerEvent) => { if (!isPrimaryPointerButton(e)) return; if ((e.target as HTMLElement).closest('button')) return; e.preventDefault(); e.stopPropagation(); const el = e.currentTarget as HTMLElement; try { el.setPointerCapture(e.pointerId); } catch { /* ignore */ } const { x, y } = clientXYFromReact(e); setDragging(true); dragOrigin.current = { mx: x, my: y, px: pos.x, py: pos.y }; unbindDragRef.current?.(); unbindDragRef.current = bindWindowDrag( ({ x: cx, y: cy }) => { const o = dragOrigin.current; setPos({ x: o.px + (cx - o.mx), y: o.py + (cy - o.my), }); }, endDrag, ); }, [pos, endDrag]); useEffect(() => () => { unbindDragRef.current?.(); }, []); const panelStyle: React.CSSProperties = { position: 'fixed', left: pos.x, top: pos.y, ...(centerOnMount && !positionReady ? { visibility: 'hidden' as const } : null), }; return { panelRef: internalRef, pos, setPos, dragging, /** @deprecated onHeaderPointerDown 사용 */ onHeaderMouseDown: onHeaderPointerDown, onHeaderPointerDown, panelStyle, headerCursor: dragging ? 'grabbing' : 'grab', headerTouchStyle: { touchAction: 'none' as const }, }; }