Files
paint-ui/components/ui/context-menu.tsx

124 lines
3.4 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useRef } from 'react';
import { useContextMenuStore } from '@/store/context-menu-store';
import { cn } from '@/lib/utils';
export function ContextMenu() {
const { isOpen, position, items, hideContextMenu } = useContextMenuStore();
const menuRef = useRef<HTMLDivElement>(null);
// Close menu when clicking outside
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
hideContextMenu();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
hideContextMenu();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen, hideContextMenu]);
// Adjust position to keep menu on screen
useEffect(() => {
if (!isOpen || !position || !menuRef.current) return;
const menu = menuRef.current;
const rect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let { x, y } = position;
// Adjust horizontal position
if (x + rect.width > viewportWidth) {
x = viewportWidth - rect.width - 8;
}
// Adjust vertical position
if (y + rect.height > viewportHeight) {
y = viewportHeight - rect.height - 8;
}
menu.style.left = `${Math.max(8, x)}px`;
menu.style.top = `${Math.max(8, y)}px`;
}, [isOpen, position]);
if (!isOpen || !position) return null;
return (
<>
{/* Backdrop to capture clicks */}
<div
className="fixed inset-0 z-[9998]"
aria-hidden="true"
/>
{/* Context Menu */}
<div
ref={menuRef}
className="fixed z-[9999] min-w-[200px] bg-card border border-border rounded-md shadow-lg py-1 animate-fadeIn"
style={{
left: `${position.x}px`,
top: `${position.y}px`,
}}
role="menu"
aria-label="Context menu"
>
{items.map((item, index) => {
if (item.separator) {
return (
<div
key={`separator-${index}`}
className="h-px bg-border my-1"
role="separator"
/>
);
}
return (
<button
key={index}
onClick={() => {
if (!item.disabled) {
item.onClick();
hideContextMenu();
}
}}
disabled={item.disabled}
className={cn(
'flex items-center gap-3 w-full px-4 py-2 text-sm text-left transition-colors',
'focus:outline-none focus:bg-accent',
item.disabled
? 'text-muted-foreground cursor-not-allowed opacity-50'
: item.danger
? 'text-destructive hover:bg-destructive/10'
: 'hover:bg-accent'
)}
role="menuitem"
aria-disabled={item.disabled}
>
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
<span>{item.label}</span>
</button>
);
})}
</div>
</>
);
}