import { useEffect, useRef, useState, useCallback } from 'react'; interface ContextMenuItem { label: string; icon?: string; danger?: boolean; disabled?: boolean; onClick: () => void; } interface ContextMenuState { x: number; y: number; items: ContextMenuItem[]; } export function useContextMenu() { const [menu, setMenu] = useState(null); const menuRef = useRef(null); const showMenu = useCallback((e: React.MouseEvent, items: ContextMenuItem[]) => { e.preventDefault(); e.stopPropagation(); // Position: use cursor, but keep within viewport const x = Math.min(e.clientX, window.innerWidth - 200); const y = Math.min(e.clientY, window.innerHeight - (items.length * 32 + 16)); setMenu({ x, y, items }); }, []); const hideMenu = useCallback(() => setMenu(null), []); // Close on outside click useEffect(() => { if (!menu) return; const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { hideMenu(); } }; // Use mousedown so it fires before the click that opened a new menu document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [menu, hideMenu]); // Close on Escape useEffect(() => { if (!menu) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') hideMenu(); }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [menu, hideMenu]); const MenuPortal = menu ? (
{menu.items.map((item, i) => ( ))}
) : null; return { showMenu, hideMenu, MenuPortal }; }