{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dropdown-menu",
  "type": "registry:ui",
  "title": "Dropdown Menu",
  "description": "An accessible React dropdown menu for Next.js with a highlight that glides between items, shortcuts, checkbox and radio items and sub-menus, built on Radix.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "lucide-react@^1.52.0",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/dropdown-menu.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { duration, ease, spring } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva } from \"class-variance-authority\";\nimport { ChevronRightIcon } from \"lucide-react\";\nimport { type HTMLMotionProps, LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { DropdownMenu as DropdownMenuPrimitive, Slot } from \"radix-ui\";\nimport {\n  type ComponentProps,\n  createContext,\n  type FocusEvent,\n  type FocusEventHandler,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\n/*\n * Dropdown Menu, built on Radix Dropdown Menu (the WAI-ARIA menu button: arrow keys, Home/End, typeahead,\n * sub-menus on the arrow keys, Escape). Context Menu builds on the pieces exported at the end, so the two\n * menus look and move the same.\n * - The surface is Select's: the same border, shadow and keyframes, opening from the trigger and closing back\n *   into it through the blur.\n * - One highlight glides between the items of each menu level (Select's layoutId pill, on spring.snappy).\n *   Radix focuses items on hover and on arrow keys, so it follows both. When the pointer leaves for a label, a\n *   separator or outside the menu, it fades out where it was. Back on a row while it's still fading, it glides\n *   on from there; once it's gone, it fades in at the new row instead of flying over from the old one.\n * - A menu and its sub-menus share that one highlight: it stays on a sub-menu's trigger while its sub-menu is\n *   open, then travels across into the sub-menu's row (and back), never a second one. The chevron nudges towards it.\n * - On a destructive item the highlight takes a red tint, which fades in and out as it glides.\n * - Checkbox items draw their check in and erase it backwards (Checkbox's motion); radio items grow their dot.\n *   Both marks sit at the end of the row, as in Select, so labels line up with plain items.\n * - With reduced motion the highlight jumps and the marks appear at once.\n */\n\ntype Tone = \"default\" | \"destructive\";\n\ntype Highlight = {\n  /** The row the highlight sits in, or null when there's none */\n  row: string | null;\n  /** False while it fades out in the row it was last in */\n  shown: boolean;\n  /** The row's tone, and the tone of the row it came from, so the red tint fades rather than snaps */\n  tone: Tone;\n  from: Tone;\n  /**\n   * Counts appearances from nothing. Each one has its own layoutId, so the highlight fades in where it appears\n   * rather than gliding over from where it last faded out.\n   */\n  appearance: number;\n  /** The first row of an appearance fades the highlight in; the rows after it glide it */\n  fresh: boolean;\n};\n\ntype Level = {\n  highlight: Highlight;\n  take: (row: string, tone: Tone) => void;\n  release: (row: string) => void;\n  /** The fade-out has finished: remove the highlight */\n  settle: () => void;\n  /** The row is gone: remove its highlight without a fade */\n  drop: (row: string) => void;\n  reduce: boolean;\n};\n\nconst LevelContext = createContext<Level | null>(null);\n\n/**\n * One menu level (the menu, or a sub-menu) and its one highlight. It lives inside the content, which Radix\n * remounts on every opening, so each opening starts clean.\n */\nfunction MenuLevel({ children }: { children: ReactNode }) {\n  const parent = useContext(LevelContext);\n  const groupId = useId();\n  const reduce = useReducedMotion() ?? false;\n  const [highlight, setHighlight] = useState<Highlight>({\n    row: null,\n    shown: false,\n    tone: \"default\",\n    from: \"default\",\n    appearance: 0,\n    fresh: false,\n  });\n\n  const take = useCallback((row: string, tone: Tone) => {\n    setHighlight((current) => {\n      if (current.row === row && current.shown && current.tone === tone) return current;\n      const appears = current.row === null;\n      return {\n        row,\n        shown: true,\n        tone,\n        from: appears ? tone : current.tone,\n        appearance: current.appearance + (appears ? 1 : 0),\n        fresh: appears,\n      };\n    });\n  }, []);\n\n  const release = useCallback(\n    (row: string) => {\n      setHighlight((current) => {\n        if (current.row !== row) return current;\n        // With reduced motion there's no fade to wait for\n        if (reduce) return { ...current, row: null, shown: false };\n        return current.shown ? { ...current, shown: false } : current;\n      });\n    },\n    [reduce],\n  );\n\n  const settle = useCallback(() => {\n    setHighlight((current) => (current.shown ? current : { ...current, row: null }));\n  }, []);\n\n  // A row that unmounts (a sub-menu that closed) while it holds the highlight takes it away at once: its\n  // fade-out would never finish, and the next row would glide in from a highlight that no longer exists\n  const drop = useCallback((row: string) => {\n    setHighlight((current) => (current.row === row ? { ...current, row: null, shown: false } : current));\n  }, []);\n\n  const level = useMemo(\n    () => ({ highlight, take, release, settle, drop, reduce }),\n    [highlight, take, release, settle, drop, reduce],\n  );\n\n  // A sub-menu joins its parent's level instead of starting its own: the menu has one highlight, so moving\n  // from a trigger into its sub-menu carries the same highlight across, and the trigger doesn't keep another\n  if (parent) return <>{children}</>;\n\n  return (\n    <LevelContext.Provider value={level}>\n      <LayoutGroup id={groupId}>{children}</LayoutGroup>\n    </LevelContext.Provider>\n  );\n}\n\n/**\n * The highlight, inside the row that has it. Rows are not stacking contexts: it joins the surface's own layer\n * (the surface is `isolate`), so it passes behind every row's text, whichever way it glides.\n */\nfunction MenuHighlight({ row }: { row: string }) {\n  const level = useContext(LevelContext);\n  if (!level || level.highlight.row !== row) return null;\n  const { highlight, reduce, settle } = level;\n  const fade = reduce ? { duration: 0 } : { duration: duration.fast, ease: ease.out };\n  // Two fills, so the red tint crossfades with the neutral one instead of snapping as the highlight leaves\n  const fill = (tone: Tone) => ({\n    initial: { opacity: highlight.from === tone ? 1 : 0 },\n    animate: { opacity: highlight.tone === tone ? 1 : 0 },\n    transition: fade,\n  });\n\n  return (\n    <motion.span\n      layoutId={`menu-highlight-${highlight.appearance}`}\n      data-slot=\"menu-highlight\"\n      aria-hidden\n      className=\"pointer-events-none absolute inset-0 -z-10 overflow-hidden rounded-md\"\n      initial={highlight.fresh ? { opacity: 0 } : false}\n      animate={{ opacity: highlight.shown ? 1 : 0 }}\n      transition={reduce ? { duration: 0 } : { ...spring.snappy, opacity: fade }}\n      onAnimationComplete={() => {\n        if (!highlight.shown) settle();\n      }}\n    >\n      <motion.span className=\"absolute inset-0 bg-accent\" {...fill(\"default\")} />\n      <motion.span\n        className=\"absolute inset-0 bg-destructive/10 dark:bg-destructive/20\"\n        {...fill(\"destructive\")}\n      />\n    </motion.span>\n  );\n}\n\n/**\n * Connects a row to its level's highlight: the row takes it when Radix focuses it (hover or arrow keys) and\n * lets go when focus leaves the level's rows. `keep` holds on to it: a sub-menu's trigger while its sub-menu\n * is open.\n */\nfunction useMenuRow({\n  tone = \"default\",\n  keep = false,\n  onFocus,\n  onBlur,\n}: {\n  tone?: Tone;\n  keep?: boolean;\n  onFocus?: FocusEventHandler<HTMLDivElement>;\n  onBlur?: FocusEventHandler<HTMLDivElement>;\n}) {\n  const level = useContext(LevelContext);\n  const row = useId();\n  const focused = useRef(false);\n  const release = level?.release;\n  const drop = level?.drop;\n\n  // A sub-menu that closes while its trigger doesn't have focus (the pointer left for the menu's edge) lets go\n  useEffect(() => {\n    if (!keep && !focused.current) release?.(row);\n  }, [keep, release, row]);\n\n  useEffect(() => () => drop?.(row), [drop, row]);\n\n  return {\n    onFocus: (event: FocusEvent<HTMLDivElement>) => {\n      focused.current = true;\n      level?.take(row, tone);\n      onFocus?.(event);\n    },\n    onBlur: (event: FocusEvent<HTMLDivElement>) => {\n      focused.current = false;\n      // Moving to another row, of this menu or its sub-menu: that row takes the highlight, so it glides instead of fading\n      const next = event.relatedTarget;\n      const toRow =\n        next instanceof Element && next.matches(\"[role^=menuitem]\") && next.closest(\"[role=menu]\");\n      if (!toRow && !keep) level?.release(row);\n      onBlur?.(event);\n    },\n    highlight: <MenuHighlight row={row} />,\n  };\n}\n\n/** Whether the nearest sub-menu is open, for its trigger */\nconst MenuSubContext = createContext(false);\n\n/** A sub-menu's open state, controlled or not, so its trigger knows to keep the highlight while it's open */\nfunction useMenuSub({\n  open,\n  defaultOpen = false,\n  onOpenChange,\n}: {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}) {\n  const [uncontrolled, setUncontrolled] = useState(defaultOpen);\n  const current = open ?? uncontrolled;\n  return {\n    open: current,\n    onOpenChange: (next: boolean) => {\n      if (open === undefined) setUncontrolled(next);\n      onOpenChange?.(next);\n    },\n  };\n}\n\n/** The same check as Checkbox, Copy Button and Loading Button, and a dash for indeterminate */\nconst CHECK = \"M5 12.5l4.5 4.5L19 7.5\";\nconst DASH = \"M7 12h10\";\n\n/** A mark that draws itself in and erases the way it came. Its opacity goes with it, so a round cap never leaves a dot. */\nfunction Mark({ d, shown, reduce }: { d: string; shown: boolean; reduce: boolean }) {\n  return (\n    <motion.path\n      d={d}\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      initial={false}\n      animate={{ pathLength: shown ? 1 : 0, opacity: shown ? 1 : 0 }}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : shown\n            ? { pathLength: { duration: duration.base, ease: ease.out }, opacity: { duration: 0 } }\n            : { duration: duration.fast, ease: ease.in }\n      }\n    />\n  );\n}\n\n/** A checkbox item's mark. Nothing draws when a menu opens; only a change does. */\nfunction MenuCheck({ checked }: { checked?: boolean | \"indeterminate\" }) {\n  const reduce = useContext(LevelContext)?.reduce ?? false;\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden className=\"size-(--size-icon-md) text-current\">\n      <Mark d={CHECK} shown={checked === true} reduce={reduce} />\n      <Mark d={DASH} shown={checked === \"indeterminate\"} reduce={reduce} />\n    </svg>\n  );\n}\n\n/** The class names both menus share */\nconst menuClasses = {\n  // Select's floating surface. Each menu adds Radix's transform origin and available height for its own type.\n  surface:\n    \"relative isolate z-(--z-popover) min-w-32 overflow-x-hidden overflow-y-auto rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-popover-in data-[state=closed]:animate-popover-out\",\n  label: \"px-2 py-1.5 text-xs text-muted-foreground data-inset:ps-8\",\n  separator: \"pointer-events-none -mx-1 my-1 h-px bg-border\",\n  shortcut: \"ms-auto ps-4 text-xs text-muted-foreground\",\n  // Checkbox and radio items leave room at the end for their mark, like Select's options\n  choice: \"pe-8\",\n  indicator: \"pointer-events-none absolute end-2 flex size-(--size-icon-md) items-center justify-center\",\n  // A fill, so it only fades (no blur) as it grows in from half size\n  dot: \"size-2 rounded-full bg-current transition duration-(--duration-fast) ease-out data-[state=unchecked]:scale-50 data-[state=unchecked]:opacity-0\",\n  subTrigger: \"group/menu-sub-trigger data-[state=open]:text-accent-foreground\",\n  // The chevron nudges towards the sub-menu while it's open\n  chevron:\n    \"ms-auto transition-transform duration-(--duration-fast) ease-out group-data-[state=open]/menu-sub-trigger:translate-x-0.5 rtl:rotate-180 rtl:group-data-[state=open]/menu-sub-trigger:-translate-x-0.5\",\n};\n\nconst menuItemVariants = cva(\n  [\n    \"relative flex cursor-default items-center gap-2 rounded-md py-1.5 ps-2 pe-2 text-sm outline-hidden select-none data-inset:ps-8\",\n    \"data-highlighted:text-accent-foreground\",\n    \"data-disabled:pointer-events-none data-disabled:opacity-(--opacity-disabled)\",\n    \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md) [&_svg:not([class*='text-'])]:text-muted-foreground\",\n  ],\n  {\n    variants: {\n      variant: {\n        default: \"\",\n        destructive:\n          \"text-destructive data-highlighted:text-destructive [&_svg:not([class*='text-'])]:text-destructive\",\n      },\n    },\n    defaultVariants: { variant: \"default\" },\n  },\n);\n\n/**\n * The floating surface of a menu or sub-menu, and its level of the highlight. Radix renders it through\n * `asChild`, so the surface is the motion element itself: with layoutScroll the highlight stays true while a\n * long menu scrolls.\n */\nfunction MenuSurface({\n  className,\n  children,\n  ...props\n}: Omit<HTMLMotionProps<\"div\">, \"children\"> & { children?: ReactNode }) {\n  return (\n    <motion.div layoutScroll className={cn(menuClasses.surface, className)} {...props}>\n      <MenuLevel>{children}</MenuLevel>\n    </motion.div>\n  );\n}\n\nfunction DropdownMenu(props: ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal(props: ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />;\n}\n\nfunction DropdownMenuTrigger(props: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return <DropdownMenuPrimitive.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />;\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  children,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        {...props}\n        asChild\n      >\n        <MenuSurface\n          className={cn(\n            \"max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin)\",\n            className,\n          )}\n        >\n          {children}\n        </MenuSurface>\n      </DropdownMenuPrimitive.Content>\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup(props: ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />;\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset || undefined}\n      className={cn(menuClasses.label, className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  children,\n  onFocus,\n  onBlur,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  /** Lines the label up with items that start with an icon */\n  inset?: boolean;\n  /** Destructive is red, and tints the highlight red as it lands */\n  variant?: Tone;\n}) {\n  const row = useMenuRow({ tone: variant, onFocus, onBlur });\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset || undefined}\n      data-variant={variant}\n      className={cn(menuItemVariants({ variant }), className)}\n      onFocus={row.onFocus}\n      onBlur={row.onBlur}\n      {...props}\n    >\n      {row.highlight}\n      {/* With asChild the child (a link) becomes the item and the highlight goes inside it */}\n      <Slot.Slottable>{children}</Slot.Slottable>\n    </DropdownMenuPrimitive.Item>\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  onFocus,\n  onBlur,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  const row = useMenuRow({ onFocus, onBlur });\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      checked={checked}\n      className={cn(menuItemVariants(), menuClasses.choice, className)}\n      onFocus={row.onFocus}\n      onBlur={row.onBlur}\n      {...props}\n    >\n      {row.highlight}\n      <span className={menuClasses.indicator}>\n        <MenuCheck checked={checked} />\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup(props: ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return <DropdownMenuPrimitive.RadioGroup data-slot=\"dropdown-menu-radio-group\" {...props} />;\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  onFocus,\n  onBlur,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  const row = useMenuRow({ onFocus, onBlur });\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(menuItemVariants(), menuClasses.choice, className)}\n      onFocus={row.onFocus}\n      onBlur={row.onBlur}\n      {...props}\n    >\n      {row.highlight}\n      <span className={menuClasses.indicator}>\n        {/* Always mounted, so the dot can grow in and shrink away rather than appear */}\n        <DropdownMenuPrimitive.ItemIndicator forceMount className={menuClasses.dot} />\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(menuClasses.separator, className)}\n      {...props}\n    />\n  );\n}\n\n/** A shortcut hint at the end of an item. It only shows the keys; binding them is up to you. */\nfunction DropdownMenuShortcut({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span data-slot=\"dropdown-menu-shortcut\" className={cn(menuClasses.shortcut, className)} {...props} />\n  );\n}\n\nfunction DropdownMenuSub({\n  open,\n  defaultOpen,\n  onOpenChange,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  const sub = useMenuSub({ open, defaultOpen, onOpenChange });\n  return (\n    <MenuSubContext.Provider value={sub.open}>\n      <DropdownMenuPrimitive.Sub\n        data-slot=\"dropdown-menu-sub\"\n        open={sub.open}\n        onOpenChange={sub.onOpenChange}\n        {...props}\n      />\n    </MenuSubContext.Provider>\n  );\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  onFocus,\n  onBlur,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }) {\n  const open = useContext(MenuSubContext);\n  const row = useMenuRow({ keep: open, onFocus, onBlur });\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset || undefined}\n      className={cn(menuItemVariants(), menuClasses.subTrigger, className)}\n      onFocus={row.onFocus}\n      onBlur={row.onBlur}\n      {...props}\n    >\n      {row.highlight}\n      {children}\n      <ChevronRightIcon aria-hidden className={menuClasses.chevron} />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  sideOffset = 4,\n  // The surface's padding and border, so the sub-menu's first item lines up with its trigger\n  alignOffset = -5,\n  children,\n  ...props\n}: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    // In a portal, so the menu's own scrolling and its keyframes' transform can't clip or carry it\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.SubContent\n        data-slot=\"dropdown-menu-sub-content\"\n        sideOffset={sideOffset}\n        alignOffset={alignOffset}\n        {...props}\n        asChild\n      >\n        <MenuSurface\n          className={cn(\n            \"max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) shadow-lg\",\n            className,\n          )}\n        >\n          {children}\n        </MenuSurface>\n      </DropdownMenuPrimitive.SubContent>\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\n// The pieces Context Menu builds on (context-menu.tsx), so the two menus never drift apart\nexport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuPortal,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n  MenuCheck,\n  MenuSubContext,\n  MenuSurface,\n  menuClasses,\n  menuItemVariants,\n  useMenuRow,\n  useMenuSub,\n};\n"
    }
  ],
  "categories": [
    "overlays"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/dropdown-menu"
}
