import { useEffect, useRef, useCallback } from 'react' import { createPortal } from 'react-dom' import { Lock } from 'lucide-react' /** * RadialMenu - ATAK-style radial context menu * Themed to match Navi light/dark palette using CSS custom properties. * * Props: * - open: boolean * - x, y: screen coordinates of trigger point * - lat, lon: geographic coordinates * - wedges: array of { id, label, icon: LucideIcon, onSelect, requiresAuth? } * - centerLabel: string (coords by default, replaced by reverse-geocode async) * - onDismiss: callback when menu should close */ export default function RadialMenu({ open, x, y, lat, lon, wedges = [], centerLabel, onDismiss, }) { const containerRef = useRef(null) const activeWedgeRef = useRef(null) // Geometry constants const outerRadius = 80 const innerRadius = 40 const wedgeCount = wedges.length || 6 const wedgeAngle = 360 / wedgeCount // Handle escape key useEffect(() => { if (!open) return const handleKey = (e) => { if (e.key === 'Escape') { onDismiss?.() } } window.addEventListener('keydown', handleKey) return () => window.removeEventListener('keydown', handleKey) }, [open, onDismiss]) // Calculate which wedge the pointer is over const getWedgeAtPoint = useCallback((clientX, clientY) => { const dx = clientX - x const dy = clientY - y const dist = Math.sqrt(dx * dx + dy * dy) // Inside inner radius = center (no wedge) if (dist < innerRadius) return null // Outside outer radius = no wedge if (dist > outerRadius + 20) return null // Calculate angle (0 = top, clockwise) let angle = Math.atan2(dx, -dy) * (180 / Math.PI) if (angle < 0) angle += 360 // Find which wedge const wedgeIndex = Math.floor(angle / wedgeAngle) return wedges[wedgeIndex] || null }, [x, y, wedges, wedgeAngle]) // Handle mouse/touch move for highlighting const handlePointerMove = useCallback((e) => { const clientX = e.touches ? e.touches[0].clientX : e.clientX const clientY = e.touches ? e.touches[0].clientY : e.clientY activeWedgeRef.current = getWedgeAtPoint(clientX, clientY) // Force re-render for highlight containerRef.current?.querySelectorAll('.radial-wedge').forEach((el, i) => { if (wedges[i] && wedges[i].id === activeWedgeRef.current?.id) { el.classList.add('active') } else { el.classList.remove('active') } }) }, [getWedgeAtPoint, wedges]) // Handle release const handlePointerUp = useCallback((e) => { const clientX = e.changedTouches ? e.changedTouches[0].clientX : e.clientX const clientY = e.changedTouches ? e.changedTouches[0].clientY : e.clientY const wedge = getWedgeAtPoint(clientX, clientY) if (wedge) { wedge.onSelect?.({ lat, lon }) } onDismiss?.() }, [getWedgeAtPoint, lat, lon, onDismiss]) // Handle backdrop click (dismiss menu) const handleBackdropClick = useCallback((e) => { e.stopPropagation() onDismiss?.() }, [onDismiss]) // Prevent menu container clicks from reaching backdrop const handleContainerClick = useCallback((e) => { e.stopPropagation() }, []) // Generate wedge paths const generateWedgePath = (index) => { const startAngle = (index * wedgeAngle - 90) * (Math.PI / 180) const endAngle = ((index + 1) * wedgeAngle - 90) * (Math.PI / 180) const x1 = innerRadius * Math.cos(startAngle) const y1 = innerRadius * Math.sin(startAngle) const x2 = outerRadius * Math.cos(startAngle) const y2 = outerRadius * Math.sin(startAngle) const x3 = outerRadius * Math.cos(endAngle) const y3 = outerRadius * Math.sin(endAngle) const x4 = innerRadius * Math.cos(endAngle) const y4 = innerRadius * Math.sin(endAngle) return `M ${x1} ${y1} L ${x2} ${y2} A ${outerRadius} ${outerRadius} 0 0 1 ${x3} ${y3} L ${x4} ${y4} A ${innerRadius} ${innerRadius} 0 0 0 ${x1} ${y1} Z` } // Calculate icon position for each wedge const getIconPosition = (index) => { const midAngle = ((index + 0.5) * wedgeAngle - 90) * (Math.PI / 180) const r = (innerRadius + outerRadius) / 2 return { x: r * Math.cos(midAngle), y: r * Math.sin(midAngle), } } if (!open) return null // Clamp position to viewport const padding = outerRadius + 20 const clampedX = Math.max(padding, Math.min(window.innerWidth - padding, x)) const clampedY = Math.max(padding, Math.min(window.innerHeight - padding, y)) const content = ( <> {/* Full-screen backdrop for dismiss — matches modal overlay opacity */}
{/* Radial menu container */}