{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dropdown-menu", "type": "registry:ui", "title": "Dropdown Menu", "description": "Anchored menu with items/labels/separators.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/text.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "dependencies": [ "lucide-react-native" ], "files": [ { "path": "registry/shared/ui/dropdown-menu.tsx", "type": "registry:ui", "target": "components/ui/dropdown-menu.tsx", "content": "import {\n cloneElement,\n createContext,\n isValidElement,\n type ReactElement,\n useContext,\n useRef,\n useState,\n} from 'react';\nimport {\n type GestureResponderEvent,\n Modal,\n Pressable,\n ScrollView,\n useWindowDimensions,\n View,\n type PressableProps,\n type ViewProps,\n} from 'react-native';\nimport { Check } from 'lucide-react-native';\nimport { Text, type TextProps } from '@/components/ui/text';\nimport { cn, composeRefs, useIconColor } from '@/lib/utils';\n\ninterface Anchor {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nconst MenuContext = createContext<{\n open: boolean;\n setOpen: (v: boolean) => void;\n anchor: Anchor | null;\n setAnchor: (a: Anchor | null) => void;\n}>({ open: false, setOpen: () => {}, anchor: null, setAnchor: () => {} });\n\nexport function DropdownMenu({ children }: { children: React.ReactNode }) {\n const [open, setOpen] = useState(false);\n const [anchor, setAnchor] = useState(null);\n return (\n \n {children}\n \n );\n}\n\nexport function DropdownMenuTrigger({\n className,\n asChild,\n children,\n onPress,\n ...props\n}: Omit & {\n className?: string;\n /** Clone the child instead of wrapping it — avoids a nested Pressable\n * swallowing the press (shadcn/radix Slot pattern). */\n asChild?: boolean;\n children?: React.ReactNode;\n}) {\n const { setOpen, setAnchor } = useContext(MenuContext);\n const ref = useRef(null);\n const handlePress = (e: GestureResponderEvent) => {\n onPress?.(e);\n ref.current?.measureInWindow((x, y, width, height) => {\n setAnchor({ x, y, width, height });\n setOpen(true);\n });\n };\n if (asChild && isValidElement(children)) {\n const child = children as ReactElement>;\n const childProps = child.props as {\n onPress?: (e: GestureResponderEvent) => void;\n className?: string;\n ref?: React.Ref;\n };\n return cloneElement(child, {\n ...props,\n ref: composeRefs(ref, childProps.ref),\n collapsable: false,\n className: cn(className, childProps.className),\n onPress: (e: GestureResponderEvent) => {\n childProps.onPress?.(e);\n handlePress(e);\n },\n });\n }\n return (\n \n {children}\n \n );\n}\n\nexport function DropdownMenuContent({\n className,\n children,\n}: {\n className?: string;\n children?: React.ReactNode;\n}) {\n const { open, setOpen, anchor } = useContext(MenuContext);\n const { width: screenW, height: screenH } = useWindowDimensions();\n const [menuH, setMenuH] = useState(0);\n const left = anchor ? Math.max(8, Math.min(anchor.x, screenW - 192 - 8)) : 0;\n const below = anchor ? anchor.y + anchor.height + 4 : 0;\n // Flip above the trigger when the menu would overflow the bottom edge.\n const top =\n anchor && menuH > 0 && below + menuH > screenH - 8\n ? Math.max(8, anchor.y - menuH - 4)\n : below;\n return (\n \n setOpen(false)}>\n {anchor && (\n e.stopPropagation()}\n style={{\n position: 'absolute',\n left,\n top,\n opacity: menuH ? 1 : 0,\n }}\n >\n setMenuH(e.nativeEvent.layout.height)}\n className={cn(\n 'max-h-72 w-48 rounded-md border border-border bg-popover p-1 shadow-md',\n className\n )}\n >\n {children}\n \n \n )}\n \n \n );\n}\n\nexport interface DropdownMenuItemProps\n extends Omit {\n className?: string;\n /** Indent to align with checkbox/radio items. */\n inset?: boolean;\n /** Red destructive action styling. */\n destructive?: boolean;\n children?: React.ReactNode;\n}\n\nexport function DropdownMenuItem({\n className,\n children,\n onPress,\n disabled,\n inset,\n destructive,\n ...props\n}: DropdownMenuItemProps) {\n const { setOpen } = useContext(MenuContext);\n return (\n {\n onPress?.(e);\n setOpen(false);\n }}\n className={cn(\n 'flex-row items-center gap-2 rounded-sm px-2 py-2 active:bg-accent',\n inset && 'pl-8',\n disabled && 'opacity-50',\n className\n )}\n {...props}\n >\n {typeof children === 'string' ? (\n \n {children}\n \n ) : (\n children\n )}\n \n );\n}\n\nexport interface DropdownMenuCheckboxItemProps\n extends Omit {\n checked?: boolean;\n onCheckedChange?: (checked: boolean) => void;\n /** Keep the menu open after toggling. */\n closeOnPress?: boolean;\n className?: string;\n children?: React.ReactNode;\n}\n\nexport function DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n onCheckedChange,\n closeOnPress = true,\n disabled,\n ...props\n}: DropdownMenuCheckboxItemProps) {\n const { setOpen } = useContext(MenuContext);\n const checkColor = useIconColor('foreground');\n return (\n {\n onCheckedChange?.(!checked);\n if (closeOnPress) setOpen(false);\n }}\n className={cn(\n 'flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 active:bg-accent',\n disabled && 'opacity-50',\n className\n )}\n {...props}\n >\n \n {checked && }\n \n {typeof children === 'string' ? (\n {children}\n ) : (\n children\n )}\n \n );\n}\n\nexport function DropdownMenuRadioGroup({\n children,\n}: {\n children?: React.ReactNode;\n}) {\n return <>{children};\n}\n\nexport interface DropdownMenuRadioItemProps\n extends Omit {\n value: string;\n /** Value currently selected in the group — controls the dot indicator. */\n selectedValue?: string;\n onValueChange?: (value: string) => void;\n closeOnPress?: boolean;\n className?: string;\n children?: React.ReactNode;\n}\n\nexport function DropdownMenuRadioItem({\n className,\n children,\n value,\n selectedValue,\n onValueChange,\n closeOnPress = true,\n disabled,\n ...props\n}: DropdownMenuRadioItemProps) {\n const { setOpen } = useContext(MenuContext);\n const colors = useIconColor('foreground');\n const selected = value === selectedValue;\n return (\n {\n onValueChange?.(value);\n if (closeOnPress) setOpen(false);\n }}\n className={cn(\n 'flex-row items-center gap-2 rounded-sm py-2 pl-8 pr-2 active:bg-accent',\n disabled && 'opacity-50',\n className\n )}\n {...props}\n >\n \n {selected && (\n \n )}\n \n {typeof children === 'string' ? (\n {children}\n ) : (\n children\n )}\n \n );\n}\n\nexport function DropdownMenuShortcut({ className, ...props }: TextProps) {\n return (\n \n );\n}\n\nexport function DropdownMenuLabel({ className, ...props }: TextProps) {\n return (\n \n );\n}\n\nexport function DropdownMenuSeparator({\n className,\n ...props\n}: ViewProps & { className?: string }) {\n return ;\n}\n" } ] }