import { useCallback, useEffect, useRef } from 'react';
import './ContextMenu.css';
export interface ContextMenuAction {
label: string;
icon: string;
disabled?: boolean;
onClick: () => void;
}
export interface ContextMenuProps {
x: number;
y: number;
actions: ContextMenuAction[];
dividerAfter?: number[];
onClose: () => void;
}
export default function ContextMenu({
x,
y,
actions,
dividerAfter = [],
onClose,
}: ContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
// Adjust position so menu stays within viewport
const adjustedPosition = useCallback(() => {
const el = menuRef.current;
if (!el) return { left: x, top: y };
const rect = el.getBoundingClientRect();
const left = x + rect.width > window.innerWidth ? x - rect.width : x;
const top = y + rect.height > window.innerHeight ? y - rect.height : y;
return { left: Math.max(0, left), top: Math.max(0, top) };
}, [x, y]);
// Close on click outside
useEffect(() => {
const handler = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
// Use capture so we catch clicks before any other handler
document.addEventListener('mousedown', handler, true);
return () => document.removeEventListener('mousedown', handler, true);
}, [onClose]);
// Close on Escape
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [onClose]);
// After mount, adjust position
useEffect(() => {
const el = menuRef.current;
if (!el) return;
const pos = adjustedPosition();
el.style.left = `${pos.left}px`;
el.style.top = `${pos.top}px`;
}, [adjustedPosition]);
const dividerSet = new Set(dividerAfter);
return (
<div
ref={menuRef}
className="ctx-menu"
style={{ left: x, top: y }}
role="menu"
>
{actions.map((action, i) => (
<div key={i}>
<button
className={`ctx-menu-item ${action.disabled ? 'ctx-menu-item-disabled' : ''}`}
role="menuitem"
disabled={action.disabled}
onClick={() => {
if (!action.disabled) {
action.onClick();
onClose();
}
}}
>
<span className="ctx-menu-icon">{action.icon}</span>
<span className="ctx-menu-label">{action.label}</span>
</button>
{dividerSet.has(i) && <div className="ctx-menu-divider" />}
</div>
))}
</div>
);
}