Popover Gooey Popover
beui/popoverFreeComponent
Composable Popover, PopoverTrigger, PopoverContent; the panel oozes out of the trigger through an SVG goo filter with a liquid neck, crisp content fading in on top. Click or hover, controlled or uncontrolled.
Install it
npx shadcn@latest add https://beui.dev/r/popover.jsonSource
1"use client";2// beui.dev/components/motion/popover34import {5 animate,6 type MotionValue,7 useMotionValue,8 useMotionValueEvent,9 useReducedMotion,10} from "motion/react";11import {12 cloneElement,13 createContext,14 isValidElement,15 type ReactElement,16 type ReactNode,17 type Ref,18 useCallback,19 useContext,20 useEffect,21 useId,22 useLayoutEffect,23 useMemo,24 useRef,25 useState,26} from "react";27import { createPortal } from "react-dom";28import { usePopoverPortalPosition } from "@/components/motion/popover-position";29import { cn } from "@/lib/utils";3031type Side = "top" | "bottom";32type Align = "start" | "center" | "end";33type TriggerMode = "click" | "hover";3435// This morph needs less bounce than layout motion: too much overshoot makes36// the liquid neck balloon past the final panel edges.37const GOO_OPEN_SPRING = {38 type: "spring",39 visualDuration: 0.3,40 bounce: 0.15,41} as const;42const GOO_CLOSE_SPRING = {43 type: "spring",44 visualDuration: 0.21,45 bounce: 0.15,46} as const;47const HOVER_CLOSE_DELAY = 120;48const CIRCLE_KAPPA = 0.5523;4950const lerp = (a: number, b: number, t: number) => a + (b - a) * t;5152interface Rect {53 x: number;54 y: number;55 w: number;56 h: number;57 r: number;58}59interface Geo {60 layerW: number;61 layerH: number;62 left: number;63 top: number;64 trigger: Rect;65 panel: Rect;66}6768// Trigger rect and panel rect in a shared local coordinate box.69function buildGeo(70 tW: number,71 tH: number,72 cW: number,73 cH: number,74 side: Side,75 align: Align,76 gap: number,77 panelRadius: number,78): Geo {79 const py = side === "bottom" ? tH + gap : -(gap + cH);80 const px = align === "start" ? 0 : align === "end" ? tW - cW : (tW - cW) / 2;8182 const left = Math.min(0, px);83 const top = Math.min(0, py);84 const layerW = Math.max(tW, px + cW) - left;85 const layerH = Math.max(tH, py + cH) - top;8687 const triggerRadius = Math.min(tH / 2, panelRadius);8889 return {90 layerW,91 layerH,92 left,93 top,94 trigger: { x: -left, y: -top, w: tW, h: tH, r: triggerRadius },95 panel: { x: px - left, y: py - top, w: cW, h: cH, r: panelRadius },96 };97}9899function rectAtProgress(geo: Geo, progress: number): Rect {100 const trigger = geo.trigger;101 const panel = geo.panel;102103 return {104 x: lerp(trigger.x, panel.x, progress),105 y: lerp(trigger.y, panel.y, progress),106 w: lerp(trigger.w, panel.w, progress),107 h: lerp(trigger.h, panel.h, progress),108 r: lerp(trigger.r, panel.r, progress),109 };110}111112// … truncated
What it pulls in
npm packages
Files it writes
More from beui
All 104 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Action Swap Bluraction-swap-blur | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Cascadeaction-swap-cascade | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Rollaction-swap-roll | beui | Components | Free | 3 deps · 4 files | |
| Agent Activityagent-activity | beui | Components | Free | 4 deps · 9 files | |
| Agent Loading States Agent Progressagent-progress | beui | Components | Free | 3 deps · 3 files | |
| AI Sidebarai-sidebar | beui | Components | Free | 4 deps · 5 files | |
| Animated Badgeanimated-badge | beui | Components | Free | 4 deps · 3 files | |
| Number Animation Animated Numberanimated-number | beui | Components | Free | 3 deps · 3 files |
