import React, { useCallback, isValidElement } from 'react'; import * as RadixMenu from '@radix-ui/react-dropdown-menu'; import { ChevronRight } from 'lucide-react'; import './Menu.css'; /** * Menu — floating action menu triggered by a child element. * Backed by @radix-ui/react-dropdown-menu for keyboard navigation, * collision-aware positioning, and proper ARIA attributes. * * @param children exactly one child — the trigger element * @param items array of items or 'separator' strings * @param placement 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' * @param open controlled open state (optional) * @param onOpenChange callback on open/close * @param width optional fixed width (px) * @param disabled disable the trigger */ export default function Menu({ children, items = [], placement = 'bottom-start', open: controlledOpen, onOpenChange, width, disabled = false, }) { // Map our placement string to Radix side + align const sideMap = { 'bottom-start': { side: 'bottom', align: 'start' }, 'bottom-end': { side: 'bottom', align: 'end' }, 'top-start': { side: 'top', align: 'start' }, 'top-end': { side: 'top', align: 'end' }, }; const { side, align } = sideMap[placement] || sideMap['bottom-start']; if (!isValidElement(children)) { return children ?? null; } const rootProps = {}; if (controlledOpen != null) { rootProps.open = controlledOpen; rootProps.onOpenChange = onOpenChange; } else if (onOpenChange) { rootProps.onOpenChange = onOpenChange; } return ( {children} {items.map((item, i) => { if (item === 'separator' || item?.type === 'separator') { return ; } const Icon = item.icon; return ( item.onSelect?.()} > {Icon && } {item.label} {item.shortcut && {item.shortcut}} {item.trailing} ); })} ); }