{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "menu",
  "title": "Menu",
  "description": "A composable native menu built on Expo UI, with support for groups, separators, and nested submenus.",
  "dependencies": [
    "@expo/ui",
    "@expo/material-symbols"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "ui/menu/index.ts",
      "content": "export * from \"./menu\";\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/menu/menu.types.ts",
      "content": "import type { IconProps as IconPrimitiveProps } from \"@expo/ui\";\n\nexport type MenuItemVariant = \"default\" | \"destructive\";\n\nexport type MenuProps = {\n  children: React.ReactNode;\n};\n\nexport type MenuContentProps = {\n  children: React.ReactNode;\n};\n\nexport type MenuTriggerProps = {\n  children: React.ReactElement;\n};\n\nexport type MenuGroupProps = {\n  direction?: \"horizontal\" | \"vertical\";\n  title?: string;\n  children: React.ReactNode;\n};\n\nexport type MenuItemProps = {\n  disabled?: boolean;\n  variant?: MenuItemVariant;\n  selected?: boolean;\n  onPress?: () => void;\n  children: React.ReactElement | React.ReactElement[];\n};\n\nexport type MenuItemLabelProps = {\n  children: string;\n};\n\nexport type MenuItemIconProps = {\n  icon: IconPrimitiveProps[\"name\"];\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/menu/menu.tsx",
      "content": "import type {\n  MenuContentProps,\n  MenuGroupProps,\n  MenuItemIconProps,\n  MenuItemLabelProps,\n  MenuItemProps,\n  MenuProps,\n  MenuTriggerProps,\n} from \"./menu.types\";\n\n// Components\nexport const MenuItem = (_: MenuItemProps) => {\n  return null;\n};\n\nexport const MenuItemLabel = (_: MenuItemLabelProps) => {\n  return null;\n};\n\nexport const MenuItemIcon = (_: MenuItemIconProps) => {\n  return null;\n};\n\nexport const MenuSeparator = () => {\n  return null;\n};\n\nexport const MenuGroup = (_: MenuGroupProps) => {\n  return null;\n};\n\nexport const MenuTrigger = (_: MenuTriggerProps) => {\n  return null;\n};\n\nMenuTrigger.displayName = \"MenuTrigger\";\n\nexport const MenuContent = (_: MenuContentProps) => {\n  return null;\n};\n\nexport const Menu = (_: MenuProps) => {\n  return null;\n};\n\nexport const MenuSubTrigger = (_: MenuTriggerProps) => {\n  return null;\n};\n\nMenuSubTrigger.displayName = \"MenuSubTrigger\";\n\nexport const MenuSubContent = (_: MenuContentProps) => {\n  return null;\n};\n\nMenuSubContent.displayName = \"MenuSubContent\";\n\nexport const MenuSub = (_: MenuProps) => {\n  return null;\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/menu/menu.ios.tsx",
      "content": "import {\n  Host as HostPrimitive,\n  Icon as IconPrimitive,\n  Text as TextPrimitive,\n} from \"@expo/ui\";\nimport {\n  Button as ButtonPrimitive,\n  ControlGroup as ControlGroupPrimitive,\n  Divider as DividerPrimitive,\n  Menu as MenuPrimitive,\n  RNHostView,\n  Section as SectionPrimitive,\n  Toggle as TogglePrimitive,\n} from \"@expo/ui/swift-ui\";\nimport {\n  disabled as disabledModifier,\n  type ModifierConfig,\n} from \"@expo/ui/swift-ui/modifiers\";\nimport { Children, isValidElement, useMemo } from \"react\";\n\nimport type {\n  MenuContentProps,\n  MenuGroupProps,\n  MenuItemIconProps,\n  MenuItemLabelProps,\n  MenuItemProps,\n  MenuProps,\n  MenuTriggerProps,\n} from \"./menu.types\";\n\n// Types\ntype MenuItemToggleProps = {\n  children: React.ReactElement | React.ReactElement[];\n  selected: boolean;\n  onPress?: () => void;\n  modifiers?: ModifierConfig[];\n};\n\n// Components\nexport const MenuItemLabel = ({ children }: MenuItemLabelProps) => {\n  return <TextPrimitive>{children}</TextPrimitive>;\n};\n\nMenuItemLabel.displayName = \"MenuItemLabel\";\n\nexport const MenuItemIcon = ({ icon }: MenuItemIconProps) => {\n  return <IconPrimitive name={icon} />;\n};\n\nMenuItemIcon.displayName = \"MenuItemIcon\";\n\nconst MenuItemToggle = ({\n  selected,\n  onPress,\n  children,\n  modifiers,\n}: MenuItemToggleProps) => {\n  const selectableParts = useMemo(\n    () => extractSelectableItemParts(children),\n    [children]\n  );\n\n  return (\n    <TogglePrimitive\n      isOn={selected}\n      label={selectableParts.label}\n      modifiers={modifiers}\n      onIsOnChange={onPress}\n      systemImage={\n        typeof selectableParts.systemImage === \"string\"\n          ? selectableParts.systemImage\n          : undefined\n      }\n    />\n  );\n};\nexport const MenuItem = ({\n  variant = \"default\",\n  disabled,\n  selected,\n  onPress,\n  children,\n}: MenuItemProps) => {\n  const modifiers = disabled ? [disabledModifier(true)] : undefined;\n\n  if (selected !== undefined) {\n    return (\n      <MenuItemToggle\n        modifiers={modifiers}\n        onPress={onPress}\n        selected={selected}\n      >\n        {children}\n      </MenuItemToggle>\n    );\n  }\n\n  return (\n    <ButtonPrimitive modifiers={modifiers} onPress={onPress} role={variant}>\n      {children}\n    </ButtonPrimitive>\n  );\n};\n\nexport const MenuSeparator = DividerPrimitive;\n\nexport const MenuGroup = ({\n  direction = \"vertical\",\n  ...props\n}: MenuGroupProps) => {\n  if (direction === \"horizontal\") {\n    return <ControlGroupPrimitive {...props} label={props.title} />;\n  }\n\n  return <SectionPrimitive {...props} />;\n};\n\nexport const MenuTrigger = ({ children, ...props }: MenuTriggerProps) => {\n  return (\n    <RNHostView matchContents {...props}>\n      {children}\n    </RNHostView>\n  );\n};\n\nMenuTrigger.displayName = \"MenuTrigger\";\n\nexport const MenuContent = ({ children }: MenuContentProps) => {\n  return children;\n};\n\nMenuContent.displayName = \"MenuContent\";\n\nexport const Menu = ({ children, ...props }: MenuProps) => {\n  const { trigger, content } = useMemo(\n    () => splitMenuChildren(children),\n    [children]\n  );\n\n  return (\n    <HostPrimitive matchContents>\n      <MenuPrimitive {...props} label={trigger}>\n        {content}\n      </MenuPrimitive>\n    </HostPrimitive>\n  );\n};\n\nexport const MenuSubTrigger = ({ children, ...props }: MenuTriggerProps) => {\n  return (\n    <RNHostView matchContents {...props}>\n      {children}\n    </RNHostView>\n  );\n};\n\nMenuSubTrigger.displayName = \"MenuSubTrigger\";\n\nexport const MenuSubContent = ({ children }: MenuContentProps) => {\n  return children;\n};\n\nMenuSubContent.displayName = \"MenuSubContent\";\n\nexport const MenuSub = ({ children, ...props }: MenuProps) => {\n  const { trigger, content } = useMemo(\n    () => splitMenuChildren(children, \"MenuSubTrigger\", \"MenuSubContent\"),\n    [children]\n  );\n\n  return (\n    <MenuPrimitive {...props} label={trigger}>\n      {content}\n    </MenuPrimitive>\n  );\n};\n\n// Utils\nconst extractSelectableItemParts = (\n  children: React.ReactElement | React.ReactElement[]\n) => {\n  let label: string | undefined;\n  let systemImage: MenuItemIconProps[\"icon\"] | undefined;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n\n    const displayName = getMenuChildDisplayName(child);\n\n    if (displayName === \"MenuItemIcon\") {\n      systemImage = (child.props as MenuItemIconProps).icon;\n      return;\n    }\n\n    if (displayName === \"MenuItemLabel\") {\n      label = (child.props as MenuItemLabelProps).children;\n    }\n  });\n\n  return { label, systemImage };\n};\n\nconst getMenuChildDisplayName = (child: React.ReactNode) => {\n  if (!isValidElement(child)) {\n    return;\n  }\n\n  return (child.type as { displayName?: string }).displayName;\n};\n\nconst splitMenuChildren = (\n  children: React.ReactNode,\n  triggerName = \"MenuTrigger\",\n  contentName = \"MenuContent\"\n) => {\n  let trigger: React.ReactNode = null;\n  let content: React.ReactNode = null;\n\n  Children.forEach(children, (child) => {\n    const displayName = getMenuChildDisplayName(child);\n\n    if (displayName === triggerName) {\n      trigger = child;\n      return;\n    }\n\n    if (displayName === contentName) {\n      content = child;\n    }\n  });\n\n  if (!trigger) {\n    throw new Error(\"Menu must have a trigger\");\n  }\n\n  if (!content) {\n    throw new Error(\"Menu must have a content\");\n  }\n\n  return { content, trigger };\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "ui/menu/menu.android.tsx",
      "content": "import { Icon as IconPrimitive } from \"@expo/ui\";\nimport {\n  Column,\n  DropdownMenuItem as DropdownMenuItemPrimitive,\n  DropdownMenu as DropdownMenuPrimitive,\n  HorizontalDivider as HorizontalDividerPrimitive,\n  Host as HostPrimitive,\n  RNHostView,\n  Row,\n  Text as TextPrimitive,\n} from \"@expo/ui/jetpack-compose\";\nimport {\n  defaultMinSize,\n  padding,\n  width,\n  wrapContentWidth,\n} from \"@expo/ui/jetpack-compose/modifiers\";\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport type { PressableProps } from \"react-native\";\nimport { useCSSVariable } from \"uniwind\";\nimport type {\n  MenuContentProps,\n  MenuGroupProps,\n  MenuItemIconProps,\n  MenuItemLabelProps,\n  MenuItemProps,\n  MenuItemVariant,\n  MenuProps,\n  MenuTriggerProps,\n} from \"./menu.types\";\n\nconst HORIZONTAL_MENU_ITEM_WIDTH = 48;\nconst MENU_GROUP_LABEL_MODIFIERS = [padding(12, 8, 12, 4)];\nconst MENU_GROUP_HORIZONTAL_ROW_MODIFIERS = [\n  padding(12, 0, 12, 8),\n  wrapContentWidth(),\n];\n\n// Types\ntype InternalMenuAndroidContextType = {\n  dismissAll: () => void;\n  expanded: boolean;\n  setExpanded: (expanded: boolean) => void;\n};\n\ntype InternalMenuSubAndroidContextType = {\n  dismissAll: () => void;\n  expanded: boolean;\n  setExpanded: (expanded: boolean) => void;\n};\n\n// Context\nconst MenuAndroidContext = createContext<InternalMenuAndroidContextType | null>(\n  null\n);\n\nconst useMenuAndroidContext = () => {\n  const context = useContext(MenuAndroidContext);\n  if (!context) {\n    throw new Error(\n      \"useMenuAndroidContext must be used within a Menu component\"\n    );\n  }\n  return context;\n};\n\nconst MenuSubAndroidContext =\n  createContext<InternalMenuSubAndroidContextType | null>(null);\n\nconst MenuSubTriggerContext = createContext(false);\n\nconst MenuGroupHorizontalContext = createContext(false);\n\nconst MenuItemAndroidContext = createContext<MenuItemVariant | null>(null);\n\nconst useMenuItemAndroidContext = () => {\n  const context = useContext(MenuItemAndroidContext);\n  if (!context) {\n    throw new Error(\n      \"useMenuItemAndroidContext must be used within a MenuItem component\"\n    );\n  }\n  return context;\n};\n\n// Components\nexport const MenuItem = ({\n  variant = \"default\",\n  onPress,\n  children,\n  disabled,\n  selected,\n  ...rest\n}: MenuItemProps) => {\n  const { dismissAll: dismissRootMenu } = useMenuAndroidContext();\n  const subMenu = useContext(MenuSubAndroidContext);\n  const isSubTrigger = useContext(MenuSubTriggerContext);\n  const isHorizontalGroup = useContext(MenuGroupHorizontalContext);\n  const destructiveColor = useCSSVariable(\"--color-destructive\") as string;\n  const foregroundColor = useCSSVariable(\n    variant === \"destructive\" ? \"--color-destructive\" : \"--color-foreground\"\n  ) as string;\n\n  const modifiers = isHorizontalGroup\n    ? [\n        defaultMinSize({ minHeight: 0, minWidth: 0 }),\n        wrapContentWidth(),\n        width(HORIZONTAL_MENU_ITEM_WIDTH),\n      ]\n    : undefined;\n\n  const elementColors =\n    variant === \"destructive\"\n      ? {\n          disabledLeadingIconColor: destructiveColor,\n          disabledTextColor: destructiveColor,\n          leadingIconColor: destructiveColor,\n          textColor: destructiveColor,\n        }\n      : undefined;\n\n  const handleClick = useCallback(() => {\n    if (isSubTrigger && subMenu) {\n      subMenu.setExpanded(true);\n      return;\n    }\n\n    onPress?.();\n    subMenu?.setExpanded(false);\n    dismissRootMenu();\n  }, [dismissRootMenu, isSubTrigger, onPress, subMenu]);\n\n  return (\n    <MenuItemAndroidContext.Provider value={variant}>\n      <DropdownMenuItemPrimitive\n        {...rest}\n        elementColors={elementColors}\n        enabled={!disabled}\n        modifiers={modifiers}\n        onClick={handleClick}\n      >\n        {children}\n        {selected ? (\n          <DropdownMenuItemPrimitive.TrailingIcon>\n            <TextPrimitive color={foregroundColor}>✓</TextPrimitive>\n          </DropdownMenuItemPrimitive.TrailingIcon>\n        ) : null}\n      </DropdownMenuItemPrimitive>\n    </MenuItemAndroidContext.Provider>\n  );\n};\n\nexport const MenuItemLabel = ({ children }: MenuItemLabelProps) => {\n  const variant = useMenuItemAndroidContext();\n  const foregroundColor = useCSSVariable(\n    variant === \"destructive\" ? \"--color-destructive\" : \"--color-foreground\"\n  ) as string;\n\n  return (\n    <DropdownMenuItemPrimitive.Text>\n      <TextPrimitive color={foregroundColor}>{children}</TextPrimitive>\n    </DropdownMenuItemPrimitive.Text>\n  );\n};\n\nexport const MenuItemIcon = ({ icon }: MenuItemIconProps) => {\n  const variant = useMenuItemAndroidContext();\n  const foregroundColor = useCSSVariable(\n    variant === \"destructive\" ? \"--color-destructive\" : \"--color-foreground\"\n  ) as string;\n\n  return (\n    <DropdownMenuItemPrimitive.LeadingIcon>\n      <IconPrimitive color={foregroundColor} name={icon} size={24} />\n    </DropdownMenuItemPrimitive.LeadingIcon>\n  );\n};\n\nexport const MenuSeparator = HorizontalDividerPrimitive;\n\nconst MenuGroupLabel = ({\n  children,\n  color,\n}: {\n  children: string;\n  color: string;\n}) => {\n  return (\n    <TextPrimitive color={color} modifiers={MENU_GROUP_LABEL_MODIFIERS}>\n      {children}\n    </TextPrimitive>\n  );\n};\n\nexport const MenuGroup = ({\n  direction = \"vertical\",\n  title,\n  children,\n}: MenuGroupProps) => {\n  const mutedColor = useCSSVariable(\"--color-muted-foreground\") as string;\n\n  if (direction === \"horizontal\") {\n    const row = (\n      <MenuGroupHorizontalContext.Provider value={true}>\n        <Row\n          horizontalArrangement={{ spacedBy: 8 }}\n          modifiers={MENU_GROUP_HORIZONTAL_ROW_MODIFIERS}\n          verticalAlignment=\"center\"\n        >\n          {children}\n        </Row>\n      </MenuGroupHorizontalContext.Provider>\n    );\n\n    if (title) {\n      return (\n        <Column modifiers={[wrapContentWidth()]}>\n          <MenuGroupLabel color={mutedColor}>{title}</MenuGroupLabel>\n          {row}\n        </Column>\n      );\n    }\n\n    return row;\n  }\n\n  if (title) {\n    return (\n      <Column>\n        <MenuGroupLabel color={mutedColor}>{title}</MenuGroupLabel>\n        {children}\n      </Column>\n    );\n  }\n\n  return <>{children}</>;\n};\n\nexport const MenuTrigger = ({ children, ...props }: MenuTriggerProps) => {\n  const { setExpanded } = useMenuAndroidContext();\n\n  const child = Children.only(children);\n\n  if (!child) {\n    if (__DEV__) {\n      throw new Error(\"MenuTrigger expects a single React element as children\");\n    }\n    return null;\n  }\n\n  return (\n    <DropdownMenuPrimitive.Trigger>\n      <RNHostView matchContents {...props}>\n        {cloneElement(child as React.ReactElement<PressableProps>, {\n          onPress: () => setExpanded(true),\n        })}\n      </RNHostView>\n    </DropdownMenuPrimitive.Trigger>\n  );\n};\n\nMenuTrigger.displayName = \"MenuTrigger\";\n\nexport const MenuContent = ({ children }: MenuContentProps) => {\n  return <DropdownMenuPrimitive.Items>{children}</DropdownMenuPrimitive.Items>;\n};\n\nMenuContent.displayName = \"MenuContent\";\n\nexport const Menu = ({ children, ...props }: MenuProps) => {\n  const [expanded, setExpanded] = useState(false);\n  const popoverColor = useCSSVariable(\"--color-popover\") as string;\n\n  const dismissAll = useCallback(() => setExpanded(false), []);\n  const ctx = useMemo(\n    () => ({ dismissAll, expanded, setExpanded }),\n    [dismissAll, expanded]\n  );\n\n  return (\n    <MenuAndroidContext.Provider value={ctx}>\n      <HostPrimitive matchContents>\n        <DropdownMenuPrimitive\n          {...props}\n          color={popoverColor}\n          expanded={expanded}\n          onDismissRequest={dismissAll}\n        >\n          {children}\n        </DropdownMenuPrimitive>\n      </HostPrimitive>\n    </MenuAndroidContext.Provider>\n  );\n};\n\nexport const MenuSubTrigger = ({ children }: MenuTriggerProps) => {\n  return (\n    <MenuSubTriggerContext.Provider value={true}>\n      <DropdownMenuPrimitive.Trigger>{children}</DropdownMenuPrimitive.Trigger>\n    </MenuSubTriggerContext.Provider>\n  );\n};\n\nMenuSubTrigger.displayName = \"MenuSubTrigger\";\n\nexport const MenuSubContent = ({ children }: MenuContentProps) => {\n  return <DropdownMenuPrimitive.Items>{children}</DropdownMenuPrimitive.Items>;\n};\n\nMenuSubContent.displayName = \"MenuSubContent\";\n\nexport const MenuSub = ({ children }: MenuProps) => {\n  const { dismissAll: dismissRootMenu } = useMenuAndroidContext();\n  const [expanded, setExpanded] = useState(false);\n\n  const dismissAll = useCallback(() => {\n    setExpanded(false);\n    dismissRootMenu();\n  }, [dismissRootMenu]);\n\n  const { trigger, content } = useMemo(\n    () => splitMenuChildren(children, \"MenuSubTrigger\", \"MenuSubContent\"),\n    [children]\n  );\n\n  const ctx = useMemo(\n    () => ({ dismissAll, expanded, setExpanded }),\n    [dismissAll, expanded]\n  );\n\n  return (\n    <MenuSubAndroidContext.Provider value={ctx}>\n      <DropdownMenuPrimitive\n        expanded={expanded}\n        onDismissRequest={() => setExpanded(false)}\n      >\n        {trigger}\n        {content}\n      </DropdownMenuPrimitive>\n    </MenuSubAndroidContext.Provider>\n  );\n};\n\n// Utils\nconst getMenuChildDisplayName = (child: React.ReactNode) => {\n  if (!isValidElement(child)) {\n    return;\n  }\n\n  return (child.type as { displayName?: string }).displayName;\n};\n\nconst splitMenuChildren = (\n  children: React.ReactNode,\n  triggerName = \"MenuTrigger\",\n  contentName = \"MenuContent\"\n) => {\n  let trigger: React.ReactNode = null;\n  let content: React.ReactNode = null;\n\n  Children.forEach(children, (child) => {\n    const displayName = getMenuChildDisplayName(child);\n\n    if (displayName === triggerName) {\n      trigger = child;\n      return;\n    }\n\n    if (displayName === contentName) {\n      content = child;\n    }\n  });\n\n  if (!trigger) {\n    throw new Error(\"Menu must have a trigger\");\n  }\n\n  if (!content) {\n    throw new Error(\"Menu must have a content\");\n  }\n\n  return { content, trigger };\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
