import BottomSheet from '@gorhom/bottom-sheet'; import { createContext, forwardRef, use, useMemo } from 'react'; import type { Text as RNText, StyleProp, ViewStyle } from 'react-native'; import { View } from 'react-native'; import Animated, { ReduceMotion } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { withUniwind } from 'uniwind'; import { BottomSheetContentContainer, CloseIcon, FullWindowOverlay, } from '../../helpers/components'; import { HeroText } from '../../helpers/components/hero-text'; import { AnimationSettingsProvider, useAnimationSettings, } from '../../helpers/contexts/animation-settings-context'; import { usePopupBottomSheetContentAnimation } from '../../helpers/hooks/use-popup-bottom-sheet-content-animation'; import { usePopupOverlayAnimation } from '../../helpers/hooks/use-popup-overlay-animation'; import { usePopupPopoverContentAnimation } from '../../helpers/hooks/use-popup-popover-content-animation'; import { usePopupRootAnimation } from '../../helpers/hooks/use-popup-root-animation'; import { useThemeColor } from '../../helpers/theme'; import * as PopoverPrimitives from '../../primitives/popover'; import * as PopoverPrimitivesTypes from '../../primitives/popover/popover.types'; import { useBottomSheetContentAnimation } from '../bottom-sheet/bottom-sheet.animation'; import bottomSheetStyles from '../bottom-sheet/bottom-sheet.styles'; import { ArrowSvg } from './arrow-svg'; import { PopoverAnimationProvider, usePopoverAnimation, } from './popover.animation'; import { DEFAULT_ALIGN_OFFSET, DEFAULT_INSETS, DEFAULT_OFFSET, DISPLAY_NAME, } from './popover.constants'; import popoverStyles, { styleSheet } from './popover.styles'; import type { PopoverArrowProps, PopoverCloseProps, PopoverContentBottomSheetProps, PopoverContentContextValue, PopoverContentPopoverProps, PopoverContentProps, PopoverDescriptionProps, PopoverOverlayProps, PopoverPortalProps, PopoverRootProps, PopoverTitleProps, PopoverTriggerProps, } from './popover.types'; const AnimatedOverlay = Animated.createAnimatedComponent( PopoverPrimitives.Overlay ); const AnimatedContent = Animated.createAnimatedComponent( PopoverPrimitives.Content ); const StyledBottomSheet = withUniwind(BottomSheet); const usePopover = PopoverPrimitives.useRootContext; const PopoverContentContext = createContext({ placement: undefined, }); // -------------------------------------------------- const PopoverRoot = forwardRef< PopoverPrimitivesTypes.RootRef, PopoverRootProps >( ( { children, closeDelay = 400, isOpen: isOpenProp, isDefaultOpen, onOpenChange: onOpenChangeProp, animation, ...props }, ref ) => { const { internalIsOpen, componentState, progress, isDragging, onOpenChange, isAllAnimationsDisabled, } = usePopupRootAnimation({ isOpen: isOpenProp, isDefaultOpen, onOpenChange: onOpenChangeProp, closeDelay, isDismissKeyboardOnClose: false, animation, }); const animationContextValue = useMemo( () => ({ popoverState: componentState, progress, isDragging, }), [componentState, progress, isDragging] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); return ( {children} ); } ); // -------------------------------------------------- const PopoverTrigger = forwardRef< PopoverPrimitivesTypes.TriggerRef, PopoverTriggerProps >((props, ref) => { return ; }); // -------------------------------------------------- const PopoverPortal = ({ className, children, ...props }: PopoverPortalProps) => { const animationSettingsContext = useAnimationSettings(); const animationContext = usePopoverAnimation(); const tvStyles = popoverStyles.portal({ className }); return ( {children} ); }; // -------------------------------------------------- const PopoverOverlay = forwardRef< PopoverPrimitivesTypes.OverlayRef, PopoverOverlayProps >( ( { className, style, animation, isAnimatedStyleActive = true, ...props }, ref ) => { const { progress, isDragging } = usePopoverAnimation(); const overlayClassName = popoverStyles.overlay({ className }); const { rContainerStyle } = usePopupOverlayAnimation({ progress, isDragging, animation, }); const overlayStyle = isAnimatedStyleActive ? [rContainerStyle, style] : style; return ( ); } ); // -------------------------------------------------- const PopoverContentPopover = forwardRef< PopoverPrimitivesTypes.ContentRef, PopoverContentProps & { presentation?: 'popover' } >( ( { placement = 'bottom', align = 'center', avoidCollisions = true, offset = DEFAULT_OFFSET, alignOffset = DEFAULT_ALIGN_OFFSET, className, children, style, animation, isAnimatedStyleActive = true, ...props }, ref ) => { const safeAreaInsets = useSafeAreaInsets(); const insets = { top: DEFAULT_INSETS.top + safeAreaInsets.top, bottom: DEFAULT_INSETS.bottom + safeAreaInsets.bottom, left: DEFAULT_INSETS.left + safeAreaInsets.left, right: DEFAULT_INSETS.right + safeAreaInsets.right, }; const { progress } = usePopoverAnimation(); const contentClassName = popoverStyles.popoverContent({ className, }); const { rContainerStyle } = usePopupPopoverContentAnimation({ progress, placement, animation, }); const contentStyle = isAnimatedStyleActive ? [styleSheet.contentContainer, rContainerStyle, style] : [styleSheet.contentContainer, style]; return ( {children} ); } ); // -------------------------------------------------- const PopoverContentBottomSheet = forwardRef< BottomSheet, PopoverContentProps & { presentation: 'bottom-sheet' } >( ( { children, backgroundClassName, handleIndicatorClassName, contentContainerClassName, contentContainerProps, animation, animationConfigs, ...restProps }, ref ) => { const { onOpenChange } = usePopover(); const { popoverState, progress } = usePopoverAnimation(); const { isAnimationDisabledValue } = useBottomSheetContentAnimation({ animation, }); const { animatedIndex } = usePopupBottomSheetContentAnimation({ progress, componentState: popoverState, }); const contentBackgroundClassName = bottomSheetStyles.contentBackground({ className: backgroundClassName, }); const contentHandleIndicatorClassName = bottomSheetStyles.contentHandleIndicator({ className: handleIndicatorClassName, }); const contentContainerClassNameValue = bottomSheetStyles.contentContainer({ className: contentContainerClassName, }); const onClose = () => { onOpenChange(false); restProps.onClose?.(); }; const mergedAnimationConfigs = useMemo( () => ({ ...animationConfigs, reduceMotion: isAnimationDisabledValue ? ReduceMotion.Always : animationConfigs?.reduceMotion, }), [animationConfigs, isAnimationDisabledValue] ); return ( {children} ); } ); // -------------------------------------------------- const PopoverContent = forwardRef< PopoverPrimitivesTypes.ContentRef | BottomSheet, PopoverContentProps >((props, ref) => { const presentation = props.presentation || 'popover'; if (presentation === 'bottom-sheet') { return ( } {...(props as PopoverContentBottomSheetProps)} /> ); } return ( } {...(props as PopoverContentPopoverProps)} /> ); }); // -------------------------------------------------- const PopoverClose = forwardRef< PopoverPrimitivesTypes.CloseRef, PopoverCloseProps >(({ className, children, iconProps, hitSlop = 12, ...props }, ref) => { const tvStyles = popoverStyles.close({ className }); const themeColorMuted = useThemeColor('muted'); return ( {children || ( )} ); }); // -------------------------------------------------- const PopoverTitle = forwardRef( ({ className, children, ...props }, ref) => { const tvStyles = popoverStyles.label({ className }); return ( {children} ); } ); // -------------------------------------------------- const PopoverDescription = forwardRef( ({ className, children, ...props }, ref) => { const tvStyles = popoverStyles.description({ className, }); return ( {children} ); } ); // -------------------------------------------------- const PopoverArrow = forwardRef( ( { children, style, className, height = 8, width = 16, fill, stroke, strokeWidth = 1, placement: placementLocal, strokeBaselineInset = 1, }, ref ) => { const [themeColorOverlay, themeColorBorder] = useThemeColor([ 'overlay', 'border', ]); const { triggerPosition, contentLayout } = usePopover(); const { placement: placementContext } = use(PopoverContentContext); const placement = placementLocal || placementContext; const tvStyles = popoverStyles.arrow({ className }); if ( !triggerPosition || !contentLayout || contentLayout.x === 0 || contentLayout.y === 0 || !placement ) { return null; } const arrowFill = fill || themeColorOverlay; const arrowStroke = stroke || themeColorBorder; const getArrowPosition = (): StyleProp => { const triggerCenterX = triggerPosition.pageX + triggerPosition.width / 2; const triggerCenterY = triggerPosition.pageY + triggerPosition.height / 2; const baseStyle: ViewStyle = { position: 'absolute', }; switch (placement) { case 'top': return { ...baseStyle, bottom: -height + strokeBaselineInset, left: Math.min( Math.max(12, triggerCenterX - contentLayout.x - width / 2), contentLayout.width - width - 12 ), }; case 'bottom': return { ...baseStyle, top: -height + strokeBaselineInset, left: Math.min( Math.max(12, triggerCenterX - contentLayout.x - width / 2), contentLayout.width - width - 12 ), }; case 'left': return { ...baseStyle, right: -height + strokeBaselineInset, top: Math.min( Math.max(12, triggerCenterY - contentLayout.y - width / 2), contentLayout.height - width - 12 ), }; case 'right': return { ...baseStyle, left: -height + strokeBaselineInset, top: Math.min( Math.max(12, triggerCenterY - contentLayout.y - width / 2), contentLayout.height - width - 12 ), }; default: return baseStyle; } }; const arrowPositionStyle = getArrowPosition(); return ( {children ? ( children ) : ( )} ); } ); // -------------------------------------------------- PopoverRoot.displayName = DISPLAY_NAME.ROOT; PopoverTrigger.displayName = DISPLAY_NAME.TRIGGER; PopoverPortal.displayName = DISPLAY_NAME.PORTAL; PopoverOverlay.displayName = DISPLAY_NAME.OVERLAY; PopoverContent.displayName = DISPLAY_NAME.CONTENT; PopoverClose.displayName = DISPLAY_NAME.CLOSE; PopoverTitle.displayName = DISPLAY_NAME.TITLE; PopoverDescription.displayName = DISPLAY_NAME.DESCRIPTION; PopoverArrow.displayName = DISPLAY_NAME.ARROW; /** * Compound Popover component with sub-components * * @component Popover - Main container that manages open/close state, positioning, * and provides context to child components. Handles placement, alignment, and collision detection. * * @component Popover.Trigger - Clickable element that toggles the popover visibility. * Wraps any child element with press handlers. * * @component Popover.Portal - Renders popover content in a portal layer above other content. * Ensures proper stacking and positioning. * * @component Popover.Overlay - Optional background overlay. Can be transparent or * semi-transparent to capture outside clicks. * * @component Popover.Content - Container for popover content with two presentation modes: * default floating popover with positioning and collision detection, or bottom sheet modal. * Supports arrow indicators and custom animations. * * @component Popover.Arrow - Optional arrow indicator pointing to the trigger element. * Automatically positions itself based on popover placement. * * @component Popover.Close - Close button that dismisses the popover when pressed. * Renders a default X icon if no children provided. * * @component Popover.Title - Optional title text with pre-styled typography. * * @component Popover.Description - Optional description text with muted styling. * * Props flow from Popover to sub-components via context (placement, align, offset, etc.). * The popover automatically positions itself relative to the trigger element. * * @see Full documentation: https://heroui.com/components/popover */ const Popover = Object.assign(PopoverRoot, { Trigger: PopoverTrigger, Portal: PopoverPortal, Overlay: PopoverOverlay, Content: PopoverContent, Arrow: PopoverArrow, Close: PopoverClose, Title: PopoverTitle, Description: PopoverDescription, }); export { usePopover, usePopoverAnimation }; export default Popover;