"use client"; import { useState, useCallback, useEffect, useRef } from "react"; interface Position { x: number; y: number; } interface ContextMenuState { isOpen: boolean; position: Position; data: T | null; } interface UseContextMenuReturn { contextMenu: ContextMenuState; openContextMenu: (e: React.MouseEvent, data: T) => void; closeContextMenu: () => void; menuRef: React.RefObject; } const MENU_WIDTH = 200; const MENU_HEIGHT = 320; // Approximate max height const VIEWPORT_MARGIN = 10; export function useContextMenu(): UseContextMenuReturn { const [contextMenu, setContextMenu] = useState>({ isOpen: false, position: { x: 0, y: 0 }, data: null, }); const menuRef = useRef(null); const calculatePosition = useCallback((clientX: number, clientY: number): Position => { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let x = clientX; let y = clientY; // Adjust for right edge if (x + MENU_WIDTH > viewportWidth - VIEWPORT_MARGIN) { x = viewportWidth - MENU_WIDTH - VIEWPORT_MARGIN; } // Adjust for bottom edge if (y + MENU_HEIGHT > viewportHeight - VIEWPORT_MARGIN) { y = viewportHeight - MENU_HEIGHT - VIEWPORT_MARGIN; } // Ensure minimum position x = Math.max(VIEWPORT_MARGIN, x); y = Math.max(VIEWPORT_MARGIN, y); return { x, y }; }, []); const openContextMenu = useCallback((e: React.MouseEvent, data: T) => { e.preventDefault(); e.stopPropagation(); const position = calculatePosition(e.clientX, e.clientY); setContextMenu({ isOpen: true, position, data, }); }, [calculatePosition]); const closeContextMenu = useCallback(() => { setContextMenu((prev) => ({ ...prev, isOpen: false, })); }, []); // Close on escape key, click outside, and scroll useEffect(() => { if (!contextMenu.isOpen) return; const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { closeContextMenu(); } }; const handleClickOutside = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { closeContextMenu(); } }; const handleScroll = (e: Event) => { if (menuRef.current && e.target instanceof Node && menuRef.current.contains(e.target)) { return; } closeContextMenu(); }; const handleBlur = () => { closeContextMenu(); }; // Add listeners with a slight delay to prevent immediate closing const timeoutId = setTimeout(() => { document.addEventListener("keydown", handleEscape); document.addEventListener("mousedown", handleClickOutside); document.addEventListener("scroll", handleScroll, true); window.addEventListener("blur", handleBlur); }, 0); return () => { clearTimeout(timeoutId); document.removeEventListener("keydown", handleEscape); document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("scroll", handleScroll, true); window.removeEventListener("blur", handleBlur); }; }, [contextMenu.isOpen, closeContextMenu]); return { contextMenu, openContextMenu, closeContextMenu, menuRef, }; }