Menubar
registry/pouf/menubar.tsx
import * as RMenubar from '@radix-ui/react-menubar'
import clsx from 'clsx'
import { motion, useReducedMotion } from 'framer-motion'
import { renderIcon, type IconLike } from './Icon'
export interface MenubarItem {
label: string
icon?: IconLike
onClick?: () => void
disabled?: boolean
tone?: 'down'
}
export interface MenubarMenu {
label: string
items: (MenubarItem | 'separator')[]
}
/** An application menu bar (File / Edit / View…). Each top-level label opens a
* clay menu; arrow keys move between them once one is open. */
export function Menubar({ menus }: { menus: MenubarMenu[] }) {
const reduceMotion = useReducedMotion()
return (
<RMenubar.Root className="pouf-menubar inline-flex items-center gap-[2px] p-[6px] rounded-pill bg-surface cushion-card">
{menus.map((menu) => (
<RMenubar.Menu key={menu.label}>
<RMenubar.Trigger className="pouf-menubar__trigger font-extrabold text-ink px-(--s3) py-(--s2) rounded-pill cursor-pointer data-[state=open]:bg-purple data-[state=open]:text-[var(--on-accent)] hover:bg-[rgba(201,168,255,0.25)]">
{menu.label}
</RMenubar.Trigger>
<RMenubar.Portal>
<RMenubar.Content className="pouf-menu" align="start" sideOffset={8} asChild>
<motion.div
initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.95, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: reduceMotion ? 0.01 : 0.14, ease: 'easeOut' }}
>
{menu.items.map((item, i) => {
if (item === 'separator') return <RMenubar.Separator key={i} className="pouf-menu__sep" />
return (
<RMenubar.Item
key={i}
className={clsx('pouf-menu__item', item.tone === 'down' && 'pouf-menu__item--down')}
onClick={item.onClick}
disabled={item.disabled}
>
{item.icon && renderIcon(item.icon, 'sm')}
{item.label}
</RMenubar.Item>
)
})}
</motion.div>
</RMenubar.Content>
</RMenubar.Portal>
</RMenubar.Menu>
))}
</RMenubar.Root>
)
}
- Interaction
- dropdown
DependenciesBundle weight · medium
- ui-primitives
- @radix-ui/react-menubar
- animation
- framer-motion