menu

PreviousNext
Docs
takiui

Preview

Loading preview…
registry/new-york/ui/menu.tsx
"use client"

import React from "react"
import { Check, ChevronRight } from "lucide-react"
import {
  Menu as AriaMenu,
  MenuItem as AriaMenuItem,
  MenuProps as AriaMenuProps,
  MenuSection as AriaMenuSection,
  MenuSectionProps as AriaMenuSectionProps,
  MenuTrigger as AriaMenuTrigger,
  Collection,
  composeRenderProps,
  Header,
  MenuItemProps,
  Separator,
  SeparatorProps,
} from "react-aria-components"

import { cn } from "../lib/utils"
import { dropdownItemStyles } from "./list-box"
import { Popover, PopoverProps } from "./popover"

interface MenuProps<T> extends AriaMenuProps<T> {
  placement?: PopoverProps["placement"]
}

export function MenuTrigger(
  props: React.ComponentProps<typeof AriaMenuTrigger>
) {
  return <AriaMenuTrigger {...props} data-slot="menu-trigger" />
}

export function Menu<T extends object>(props: MenuProps<T>) {
  return (
    <Popover
      placement={props.placement}
      className={cn("w-56", props.className)}
    >
      <AriaMenu
        {...props}
        className="max-h-[inherit] overflow-auto rounded-[inherit] p-1 outline-0"
      />
    </Popover>
  )
}

export function MenuItem(props: MenuItemProps) {
  const textValue =
    props.textValue ||
    (typeof props.children === "string" ? props.children : undefined)
  return (
    <AriaMenuItem
      textValue={textValue}
      {...props}
      className={composeRenderProps(props.className, (className, renderProps) =>
        dropdownItemStyles({ ...renderProps, className })
      )}
    >
      {composeRenderProps(
        props.children,
        (children, { selectionMode, isSelected, hasSubmenu }) => (
          <>
            <span className="group-selected:font-semibold flex flex-1 items-center gap-2 truncate font-normal">
              {children}
            </span>
            {selectionMode !== "none" && isSelected && (
              <Check aria-hidden className="h-4 w-4" />
            )}
            {hasSubmenu && (
              <ChevronRight aria-hidden className="absolute right-2 h-4 w-4" />
            )}
          </>
        )
      )}
    </AriaMenuItem>
  )
}

export function MenuSeparator(props: SeparatorProps) {
  return <Separator {...props} className="bg-border -mx-1 my-1 h-px" />
}

export interface MenuSectionProps<T> extends AriaMenuSectionProps<T> {
  title?: string
  items?: Iterable<T>
}

export function MenuSection<T extends object>(props: MenuSectionProps<T>) {
  return (
    <AriaMenuSection {...props}>
      {props.title && (
        <Header className="sticky -top-[5px] z-10 -mx-1 -mt-px truncate px-4 py-1 text-sm font-medium backdrop-blur-md [&+*]:mt-1">
          {props.title}
        </Header>
      )}
      <Collection items={props.items}>{props.children}</Collection>
    </AriaMenuSection>
  )
}

Installation

npx shadcn@latest add @taki/menu

Usage

import { Menu } from "@/components/ui/menu"
<Menu />