'use client'; import { useState, useId, useRef, useEffect, createContext, useContext, isValidElement, } from 'react'; import { AnimatePresence, MotionConfig, motion, type Transition, type Variants, } from 'motion/react'; import useClickOutside from '@/hooks/useClickOutside'; import { cn } from '@/lib/utils'; const TRANSITION = { type: 'spring', bounce: 0.1, duration: 0.4, }; type MorphingPopoverContextValue = { isOpen: boolean; open: () => void; close: () => void; uniqueId: string; variants?: Variants; }; const MorphingPopoverContext = createContext(null); function usePopoverLogic({ defaultOpen = false, open: controlledOpen, onOpenChange, }: { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; } = {}) { const uniqueId = useId(); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); const isOpen = controlledOpen ?? uncontrolledOpen; const open = () => { if (controlledOpen === undefined) { setUncontrolledOpen(true); } onOpenChange?.(true); }; const close = () => { if (controlledOpen === undefined) { setUncontrolledOpen(false); } onOpenChange?.(false); }; return { isOpen, open, close, uniqueId }; } export type MorphingPopoverProps = { children: React.ReactNode; transition?: Transition; defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; variants?: Variants; className?: string; } & React.ComponentProps<'div'>; function MorphingPopover({ children, transition = TRANSITION, defaultOpen, open, onOpenChange, variants, className, ...props }: MorphingPopoverProps) { const popoverLogic = usePopoverLogic({ defaultOpen, open, onOpenChange }); return (
{children}
); } export type MorphingPopoverTriggerProps = { asChild?: boolean; children: React.ReactNode; className?: string; } & React.ComponentProps; function MorphingPopoverTrigger({ children, className, asChild = false, ...props }: MorphingPopoverTriggerProps) { const context = useContext(MorphingPopoverContext); if (!context) { throw new Error( 'MorphingPopoverTrigger must be used within MorphingPopover' ); } if (asChild && isValidElement(children)) { const MotionComponent = motion.create( children.type as React.ForwardRefExoticComponent ); const childProps = children.props as Record; return ( ); } return ( {children} ); } export type MorphingPopoverContentProps = { children: React.ReactNode; className?: string; } & React.ComponentProps; function MorphingPopoverContent({ children, className, ...props }: MorphingPopoverContentProps) { const context = useContext(MorphingPopoverContext); if (!context) throw new Error( 'MorphingPopoverContent must be used within MorphingPopover' ); const ref = useRef(null); useClickOutside(ref, context.close); useEffect(() => { if (!context.isOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') context.close(); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [context.isOpen, context.close]); return ( {context.isOpen && ( <> {children} )} ); } export { MorphingPopover, MorphingPopoverTrigger, MorphingPopoverContent };