{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "popper", "type": "registry:ui", "title": "Popper", "description": "Anchor-positioned floating content with flip.", "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/popper.tsx", "type": "registry:ui", "target": "components/ui/popper.tsx", "content": "import {\n useMemo,\n useRef,\n useState,\n type ReactNode,\n type RefObject,\n} from 'react';\nimport {\n type LayoutChangeEvent,\n Modal,\n Pressable,\n useWindowDimensions,\n View,\n} from 'react-native';\nimport { cn } from '@/lib/utils';\n\nexport type PopperPlacement =\n | 'top'\n | 'top-start'\n | 'top-end'\n | 'bottom'\n | 'bottom-start'\n | 'bottom-end'\n | 'left'\n | 'left-start'\n | 'left-end'\n | 'right'\n | 'right-start'\n | 'right-end';\n\nexport interface AnchorRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nconst EDGE_MARGIN = 8;\n\n/**\n * Measures a trigger view in window coordinates via `measureInWindow`.\n * Attach `ref` to the anchor element, call `measure()` before opening.\n */\nexport function usePopperAnchor() {\n const ref = useRef(null);\n const [anchor, setAnchor] = useState(null);\n\n const measure = () => {\n ref.current?.measureInWindow((x, y, width, height) => {\n setAnchor({ x, y, width, height });\n });\n };\n\n return { ref: ref as RefObject, anchor, measure };\n}\n\ntype Side = 'top' | 'bottom' | 'left' | 'right';\ntype Align = 'start' | 'center' | 'end';\n\nfunction parsePlacement(placement: PopperPlacement): [Side, Align] {\n const [side, align] = placement.split('-') as [Side, Align?];\n return [side, align ?? 'center'];\n}\n\nfunction flipSide(side: Side): Side {\n return side === 'top'\n ? 'bottom'\n : side === 'bottom'\n ? 'top'\n : side === 'left'\n ? 'right'\n : 'left';\n}\n\nfunction resolve(\n side: Side,\n align: Align,\n anchor: AnchorRect,\n content: { width: number; height: number },\n offset: number\n) {\n let left: number;\n let top: number;\n\n switch (side) {\n case 'top':\n top = anchor.y - content.height - offset;\n left =\n align === 'start'\n ? anchor.x\n : align === 'end'\n ? anchor.x + anchor.width - content.width\n : anchor.x + anchor.width / 2 - content.width / 2;\n break;\n case 'bottom':\n top = anchor.y + anchor.height + offset;\n left =\n align === 'start'\n ? anchor.x\n : align === 'end'\n ? anchor.x + anchor.width - content.width\n : anchor.x + anchor.width / 2 - content.width / 2;\n break;\n case 'left':\n left = anchor.x - content.width - offset;\n top =\n align === 'start'\n ? anchor.y\n : align === 'end'\n ? anchor.y + anchor.height - content.height\n : anchor.y + anchor.height / 2 - content.height / 2;\n break;\n case 'right':\n left = anchor.x + anchor.width + offset;\n top =\n align === 'start'\n ? anchor.y\n : align === 'end'\n ? anchor.y + anchor.height - content.height\n : anchor.y + anchor.height / 2 - content.height / 2;\n break;\n }\n return { left, top };\n}\n\n/** True when the resolved rect overflows the screen on the placement side. */\nfunction overflows(\n side: Side,\n pos: { left: number; top: number },\n content: { width: number; height: number },\n screen: { width: number; height: number }\n) {\n switch (side) {\n case 'top':\n return pos.top < EDGE_MARGIN;\n case 'bottom':\n return pos.top + content.height > screen.height - EDGE_MARGIN;\n case 'left':\n return pos.left < EDGE_MARGIN;\n case 'right':\n return pos.left + content.width > screen.width - EDGE_MARGIN;\n }\n}\n\nexport interface PopperProps {\n open: boolean;\n /** Window rect of the anchor element (see `usePopperAnchor`). */\n anchor?: AnchorRect | null;\n placement?: PopperPlacement;\n /** Gap between anchor and content in px. Default 6. */\n sideOffset?: number;\n /** Render a small rotated-square arrow pointing at the anchor. */\n showArrow?: boolean;\n /** Flip to the opposite side when content would overflow. Default true. */\n flip?: boolean;\n /** Tapping outside requests close. Default true. */\n dismissable?: boolean;\n onClose?: () => void;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * Popper — positioning primitive anchored to a measured rect.\n * Renders inside a transparent RN `Modal`; clamps to screen edges and\n * flips on overflow. Compose inside: menu items, tooltips, etc.\n */\nexport function Popper({\n open,\n anchor,\n placement = 'bottom',\n sideOffset = 6,\n showArrow = false,\n flip = true,\n dismissable = true,\n onClose,\n children,\n className,\n}: PopperProps) {\n const { width: screenW, height: screenH } = useWindowDimensions();\n const [size, setSize] = useState({ width: 0, height: 0 });\n\n const { pos, resolvedSide, arrow } = useMemo(() => {\n if (!anchor || size.width === 0) {\n return {\n pos: { left: -9999, top: 0 },\n resolvedSide: parsePlacement(placement)[0],\n arrow: { left: 0, top: 0 },\n };\n }\n const screen = { width: screenW, height: screenH };\n let [side, align] = parsePlacement(placement);\n let p = resolve(side, align, anchor, size, sideOffset);\n\n if (flip && overflows(side, p, size, screen)) {\n const flipped = flipSide(side);\n const fp = resolve(flipped, align, anchor, size, sideOffset);\n if (!overflows(flipped, fp, size, screen)) {\n side = flipped;\n p = fp;\n }\n }\n\n // Clamp inside the window.\n const left = Math.max(\n EDGE_MARGIN,\n Math.min(p.left, screenW - size.width - EDGE_MARGIN)\n );\n const top = Math.max(\n EDGE_MARGIN,\n Math.min(p.top, screenH - size.height - EDGE_MARGIN)\n );\n\n // Arrow sits on the edge facing the anchor, centered on its cross-axis.\n const arrowPos =\n side === 'top' || side === 'bottom'\n ? {\n left: anchor.x + anchor.width / 2 - left - ARROW_SIZE / 2,\n top:\n side === 'top' ? size.height - ARROW_SIZE / 2 : -ARROW_SIZE / 2,\n }\n : {\n left:\n side === 'left' ? size.width - ARROW_SIZE / 2 : -ARROW_SIZE / 2,\n top: anchor.y + anchor.height / 2 - top - ARROW_SIZE / 2,\n };\n\n return { pos: { left, top }, resolvedSide: side, arrow: arrowPos };\n }, [anchor, size, placement, sideOffset, flip, screenW, screenH]);\n\n const handleLayout = (e: LayoutChangeEvent) => {\n const { width, height } = e.nativeEvent.layout;\n if (width !== size.width || height !== size.height) {\n setSize({ width, height });\n }\n };\n\n return (\n \n \n {anchor && (\n \n {showArrow && (\n \n )}\n e.stopPropagation()}>\n \n {children}\n \n \n \n )}\n \n \n );\n}\n\nconst ARROW_SIZE = 10;\n" } ] }