Popover Morph Popover
beui/popover-morphFreeComponent
Composable MorphPopover, MorphPopoverTrigger, MorphPopoverContent; the panel is laid out full size but clipped to the corner nearest the trigger, then unclips as one piece — a single-surface morph with a drop-shadow that hugs the shape. Side/align aware, controlled or uncontrolled.
Install it
npx shadcn@latest add https://beui.dev/r/popover-morph.jsonSource
1"use client";2// beui.dev/components/motion/popover34import { AnimatePresence, motion, useReducedMotion } from "motion/react";5import {6 cloneElement,7 createContext,8 isValidElement,9 type ReactElement,10 type ReactNode,11 type Ref,12 useCallback,13 useContext,14 useEffect,15 useId,16 useMemo,17 useRef,18 useState,19} from "react";20import { createPortal } from "react-dom";21import { usePopoverPortalPosition } from "@/components/motion/popover-position";22import { EASE_OUT, SPRING_PANEL } from "@/lib/ease";23import { cn } from "@/lib/utils";2425type Side = "top" | "bottom";26type Align = "start" | "end";2728type MorphContextValue = {29 open: boolean;30 setOpen: (open: boolean) => void;31 toggle: () => void;32 triggerId: string;33 contentId: string;34 triggerRef: React.MutableRefObject<HTMLElement | null>;35 contentRef: React.MutableRefObject<HTMLDivElement | null>;36};3738const MorphContext = createContext<MorphContextValue | null>(null);3940function useMorphContext(component: string) {41 const ctx = useContext(MorphContext);42 if (!ctx) throw new Error(`${component} must be used within <MorphPopover>`);43 return ctx;44}4546export interface MorphPopoverProps {47 children: ReactNode;48 /** Controlled open state. */49 open?: boolean;50 /** Uncontrolled initial open state. */51 defaultOpen?: boolean;52 onOpenChange?: (open: boolean) => void;53 className?: string;54}5556/**57 * A popover whose panel morphs open from the trigger corner: it's laid out at58 * full size but clipped to the corner nearest the trigger, then unclips as one59 * piece. Closes on outside pointer / Escape. Controlled or uncontrolled.60 */61export function MorphPopover({62 children,63 open: controlledOpen,64 defaultOpen = false,65 onOpenChange,66 className,67}: MorphPopoverProps) {68 const baseId = useId();69 const rootRef = useRef<HTMLDivElement>(null);70 const triggerRef = useRef<HTMLElement | null>(null);71 const contentRef = useRef<HTMLDivElement | null>(null);72 const [internalOpen, setInternalOpen] = useState(defaultOpen);73 const controlled = controlledOpen !== undefined;74 const open = controlled ? controlledOpen : internalOpen;7576 const setOpen = useCallback(77 (next: boolean) => {78 if (!controlled) setInternalOpen(next);79 onOpenChange?.(next);80 },81 [controlled, onOpenChange],82 );83 const toggle = useCallback(() => setOpen(!open), [setOpen, open]);8485 useEffect(() => {86 if (!open) return;87 const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);88// … 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 |
