{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "popover", "type": "registry:ui", "title": "Popover", "description": "Anchored overlay via measureInWindow + Modal.", "registryDependencies": [ "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/utils.json", "https://raw.githubusercontent.com/truongnat/rnui/main/registry/dist/nativewind/theme.json" ], "files": [ { "path": "registry/shared/ui/popover.tsx", "type": "registry:ui", "target": "components/ui/popover.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 useWindowDimensions,\n View,\n type PressableProps,\n type ViewProps,\n} from 'react-native';\nimport { cn, composeRefs } from '@/lib/utils';\n\ninterface Anchor {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nconst PopoverContext = 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 Popover({ 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 PopoverTrigger({\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(PopoverContext);\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 interface PopoverContentProps extends ViewProps {\n className?: string;\n /** Offset below the trigger in px. */\n sideOffset?: number;\n}\n\nexport function PopoverContent({\n className,\n sideOffset = 8,\n children,\n style,\n ...props\n}: PopoverContentProps) {\n const { open, setOpen, anchor } = useContext(PopoverContext);\n const { width: screenW, height: screenH } = useWindowDimensions();\n const [measured, setMeasured] = useState({ w: 0, h: 0 });\n const left = anchor\n ? Math.max(8, Math.min(anchor.x, screenW - (measured.w || 256) - 8))\n : 0;\n const below = anchor ? anchor.y + anchor.height + sideOffset : 0;\n // Flip above the trigger when the content would overflow the bottom edge.\n const top =\n anchor && measured.h > 0 && below + measured.h > screenH - 8\n ? Math.max(8, anchor.y - measured.h - sideOffset)\n : below;\n return (\n \n setOpen(false)}>\n {anchor && (\n e.stopPropagation()}\n style={{\n position: 'absolute',\n left,\n top,\n opacity: measured.h ? 1 : 0,\n }}\n >\n \n setMeasured({\n w: e.nativeEvent.layout.width,\n h: e.nativeEvent.layout.height,\n })\n }\n className={cn(\n 'w-64 rounded-md border border-border bg-popover p-4 shadow-md',\n className\n )}\n style={style}\n {...props}\n >\n {children}\n \n \n )}\n \n \n );\n}\n" } ] }