{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dropdown-menu","type":"registry:ui","title":"Dropdown Menu","description":"A menu of actions or options, opened by a button.","dependencies":["lucide-react","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-portal.json"],"files":[{"path":"registry/dashral/dropdown-menu.tsx","type":"registry:file","target":"components/dashral/dropdown-menu.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\"\nimport { ArrowUpRightIcon, CheckIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"./lib/utils\"\nimport { usePortalContainer } from \"./lib/portal\"\n\n/** One entry in a menu. A plain item is the default; the others say what they are with `type`. */\ntype MenuItem =\n  | {\n      type?: \"item\"\n      label: React.ReactNode\n      icon?: React.ReactNode\n      /** The keys that do the same, shown at the end: \"⌘K\". */\n      shortcut?: string\n      /** destructive is red, for deleting. */\n      color?: \"default\" | \"destructive\"\n      disabled?: boolean\n      onSelect?: (event: Event) => void\n      /** Makes the item a link (through linkAs). */\n      href?: string\n      /** With href: opens in a new tab, marked with an arrow. */\n      external?: boolean\n      /** The page you're on: highlighted, and announced as the current page. */\n      active?: boolean\n      /** Keep the menu open after it's picked. */\n      keepOpen?: boolean\n      className?: string\n    }\n  | {\n      type: \"checkbox\"\n      label: React.ReactNode\n      icon?: React.ReactNode\n      checked: boolean\n      onCheckedChange: (checked: boolean) => void\n      disabled?: boolean\n      shortcut?: string\n      /** Close the menu once it's ticked; it stays open by default, so several can be ticked. */\n      closeOnSelect?: boolean\n      className?: string\n    }\n  | {\n      type: \"radio\"\n      value: string\n      onValueChange: (value: string) => void\n      options: { value: string; label: React.ReactNode; icon?: React.ReactNode; disabled?: boolean }[]\n    }\n  | {\n      /** Small items side by side, e.g. a row of emoji reactions. Each needs an aria-label. */\n      type: \"row\"\n      /** Names the row for screen readers. */\n      label: string\n      items: { label: React.ReactNode; \"aria-label\": string; onSelect?: (event: Event) => void; disabled?: boolean }[]\n    }\n  | { type: \"label\"; label: React.ReactNode; className?: string }\n  | { type: \"separator\" }\n  | { type: \"submenu\"; label: React.ReactNode; icon?: React.ReactNode; items: MenuItem[]; disabled?: boolean }\n\nconst itemClass =\n  \"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[variant=destructive]:text-destructive-text data-[variant=destructive]:focus:bg-destructive/10 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:[&_svg:not([class*='text-'])]:text-destructive-text\"\nconst indicatorItemClass =\n  \"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pe-2 ps-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\nconst contentClass =\n  \"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:zoom-in-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:blur-in-[2px] data-[state=open]:duration-(--duration-standard) data-[state=open]:ease-spring data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:duration-(--duration-fast) data-[state=closed]:ease-exit motion-reduce:blur-in-0\"\n\n/** The parts a Radix menu has; DropdownMenu and ContextMenu both fit. */\ntype MenuPrimitives = Pick<\n  typeof DropdownMenuPrimitive,\n  \"Item\" | \"CheckboxItem\" | \"RadioGroup\" | \"RadioItem\" | \"ItemIndicator\" | \"Label\" | \"Separator\" | \"Sub\" | \"SubTrigger\" | \"SubContent\" | \"Portal\"\n>\n\nconst Shortcut = ({ children }: { children: string }) => (\n  <span className=\"ms-auto text-xs tracking-widest text-muted-foreground\">{children}</span>\n)\n\n/** Building block: renders menu items with a Radix menu's parts. ContextMenu uses it too. */\nfunction MenuItems({\n  items,\n  primitives: P,\n  linkAs: Link = \"a\",\n  panelClass = contentClass,\n}: {\n  items: MenuItem[]\n  primitives: MenuPrimitives\n  linkAs?: React.ElementType\n  /** The classes for a submenu's panel. */\n  panelClass?: string\n}) {\n  const portalContainer = usePortalContainer()\n  return items.map((item, i) => {\n    switch (item.type) {\n      case \"separator\":\n        return <P.Separator key={i} className=\"-mx-1 my-1 h-px bg-border\" />\n      case \"row\":\n        return (\n          <div key={i} role=\"group\" aria-label={item.label} className=\"flex justify-between px-1 py-1\">\n            {item.items.map((entry, j) => (\n              <P.Item\n                key={j}\n                aria-label={entry[\"aria-label\"]}\n                disabled={entry.disabled}\n                onSelect={entry.onSelect}\n                className={cn(itemClass, \"size-8 justify-center p-0 text-lg\")}\n              >\n                {entry.label}\n              </P.Item>\n            ))}\n          </div>\n        )\n      case \"label\":\n        return (\n          <P.Label key={i} className={cn(\"px-2 py-1.5 text-sm font-medium\", item.className)}>\n            {item.label}\n          </P.Label>\n        )\n      case \"checkbox\":\n        return (\n          <P.CheckboxItem\n            key={i}\n            checked={item.checked}\n            onCheckedChange={(checked) => item.onCheckedChange(checked === true)}\n            onSelect={item.closeOnSelect ? undefined : (event) => event.preventDefault()}\n            disabled={item.disabled}\n            className={cn(indicatorItemClass, item.className)}\n          >\n            <span className=\"pointer-events-none absolute start-2 flex size-3.5 items-center justify-center\">\n              <P.ItemIndicator>\n                <CheckIcon className=\"size-4\" />\n              </P.ItemIndicator>\n            </span>\n            {item.icon}\n            {item.label}\n            {item.shortcut && <Shortcut>{item.shortcut}</Shortcut>}\n          </P.CheckboxItem>\n        )\n      case \"radio\":\n        return (\n          <P.RadioGroup key={i} value={item.value} onValueChange={item.onValueChange}>\n            {item.options.map((option) => (\n              <P.RadioItem key={option.value} value={option.value} disabled={option.disabled} className={indicatorItemClass}>\n                <span className=\"pointer-events-none absolute start-2 flex size-3.5 items-center justify-center\">\n                  <P.ItemIndicator>\n                    <span className=\"block size-2 rounded-full bg-current\" />\n                  </P.ItemIndicator>\n                </span>\n                {option.icon}\n                {option.label}\n              </P.RadioItem>\n            ))}\n          </P.RadioGroup>\n        )\n      case \"submenu\":\n        return (\n          <P.Sub key={i}>\n            <P.SubTrigger disabled={item.disabled} className={cn(itemClass, \"data-[state=open]:bg-accent\")}>\n              {item.icon}\n              {item.label}\n              <ChevronRightIcon className=\"ms-auto rtl:-scale-x-100\" />\n            </P.SubTrigger>\n            <P.Portal container={portalContainer}>\n              <P.SubContent className={cn(panelClass, \"shadow-lg\")}>\n                <MenuItems items={item.items} primitives={P} linkAs={Link} panelClass={panelClass} />\n              </P.SubContent>\n            </P.Portal>\n          </P.Sub>\n        )\n      default: {\n        const inner = (\n          <>\n            {item.icon}\n            {item.label}\n            {item.shortcut && <Shortcut>{item.shortcut}</Shortcut>}\n            {item.external && <ArrowUpRightIcon className=\"ms-auto\" aria-hidden />}\n          </>\n        )\n        return (\n          <P.Item\n            key={i}\n            asChild={item.href !== undefined}\n            disabled={item.disabled}\n            data-variant={item.color ?? \"default\"}\n            onSelect={(event) => {\n              if (item.keepOpen) event.preventDefault()\n              item.onSelect?.(event)\n            }}\n            className={cn(itemClass, item.active && \"bg-accent font-medium\", item.className)}\n          >\n            {item.href === undefined ? (\n              inner\n            ) : item.external ? (\n              <a href={item.href} target=\"_blank\" rel=\"noreferrer\">\n                {inner}\n              </a>\n            ) : (\n              <Link href={item.href} aria-current={item.active ? \"page\" : undefined}>\n                {inner}\n              </Link>\n            )}\n          </P.Item>\n        )\n      }\n    }\n  })\n}\n\ntype DropdownMenuProps = Omit<React.ComponentProps<typeof DropdownMenuPrimitive.Root>, \"children\"> & {\n  /** What opens it, e.g. a button; it must take a ref and focus. */\n  trigger: React.ReactElement\n  items: MenuItem[]\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n  align?: \"start\" | \"center\" | \"end\"\n  /** Distance from the trigger, in pixels. */\n  sideOffset?: number\n  /** Your router's link, for items with href. */\n  linkAs?: React.ElementType\n  /** Called as the menu closes, before focus goes back to the trigger; preventDefault keeps it from going back. */\n  onCloseAutoFocus?: (event: Event) => void\n  /** The menu panel's classes, e.g. a width. */\n  className?: string\n}\n\n/**\n * A menu that opens from a trigger, in one tag: items with icons and shortcuts, checkboxes, radio groups, labels,\n * separators and submenus.\n */\nfunction DropdownMenu({ trigger, items, side, align, sideOffset = 4, linkAs, onCloseAutoFocus, className, ...props }: DropdownMenuProps) {\n  const portalContainer = usePortalContainer()\n  return (\n    <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props}>\n      <DropdownMenuPrimitive.Trigger asChild>{trigger}</DropdownMenuPrimitive.Trigger>\n      <DropdownMenuPrimitive.Portal container={portalContainer}>\n        <DropdownMenuPrimitive.Content\n          data-slot=\"dropdown-menu-content\"\n          side={side}\n          align={align}\n          sideOffset={sideOffset}\n          onCloseAutoFocus={onCloseAutoFocus}\n          className={cn(contentClass, className)}\n        >\n          <MenuItems items={items} primitives={DropdownMenuPrimitive} linkAs={linkAs} />\n        </DropdownMenuPrimitive.Content>\n      </DropdownMenuPrimitive.Portal>\n    </DropdownMenuPrimitive.Root>\n  )\n}\n\nexport { DropdownMenu, MenuItems, type DropdownMenuProps, type MenuItem, type MenuPrimitives }\n"}],"cssVars":{"theme":{"color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c"}}}