import BottomSheet from '@gorhom/bottom-sheet'; import { forwardRef, useCallback, useMemo } from 'react'; import type { LayoutChangeEvent, Text as RNText } from 'react-native'; import { View } from 'react-native'; import { GestureDetector } from 'react-native-gesture-handler'; import Animated, { ReduceMotion } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { withUniwind } from 'uniwind'; import { BottomSheetContentContainer, CheckIcon, 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 { usePopupDialogContentAnimation } from '../../helpers/hooks/use-popup-dialog-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 SelectPrimitives from '../../primitives/select'; import * as SelectPrimitivesTypes from '../../primitives/select/select.types'; import { useBottomSheetContentAnimation } from '../bottom-sheet/bottom-sheet.animation'; import bottomSheetStyles from '../bottom-sheet/bottom-sheet.styles'; import { SelectAnimationProvider, useSelectAnimation, } from './select.animation'; import { DEFAULT_ALIGN_OFFSET, DEFAULT_INSETS, DEFAULT_OFFSET, DISPLAY_NAME, } from './select.constants'; import selectStyles, { styleSheet } from './select.styles'; import type { SelectCloseProps, SelectContentBottomSheetProps, SelectContentDialogProps, SelectContentPopoverProps, SelectContentProps, SelectItemDescriptionProps, SelectItemIndicatorProps, SelectItemLabelProps, SelectItemProps, SelectItemRenderProps, SelectListLabelProps, SelectOverlayProps, SelectPortalProps, SelectRootProps, SelectTriggerProps, SelectValueProps, } from './select.types'; const AnimatedOverlay = Animated.createAnimatedComponent( SelectPrimitives.Overlay ); const AnimatedPopoverContent = Animated.createAnimatedComponent( SelectPrimitives.PopoverContent ); const AnimatedDialogContent = Animated.createAnimatedComponent( SelectPrimitives.DialogContent ); const StyledBottomSheet = withUniwind(BottomSheet); const useSelect = SelectPrimitives.useRootContext; const useSelectItem = SelectPrimitives.useItemContext; // -------------------------------------------------- const SelectRoot = forwardRef( ( { children, closeDelay = 400, isDismissKeyboardOnClose = true, isOpen: isOpenProp, isDefaultOpen, onOpenChange: onOpenChangeProp, animation, ...props }, ref ) => { const { internalIsOpen, componentState, progress, isDragging, isGestureReleaseAnimationRunning, onOpenChange, isAllAnimationsDisabled, } = usePopupRootAnimation({ isOpen: isOpenProp, isDefaultOpen, onOpenChange: onOpenChangeProp, closeDelay, isDismissKeyboardOnClose, animation, }); const animationContextValue = useMemo( () => ({ selectState: componentState, progress, isDragging, isGestureReleaseAnimationRunning, }), [componentState, progress, isDragging, isGestureReleaseAnimationRunning] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); return ( {children} ); } ); // -------------------------------------------------- const SelectTrigger = forwardRef< SelectPrimitivesTypes.TriggerRef, SelectTriggerProps >((props, ref) => { const { isDisabled: isDisabledProp } = props; const { isDisabled } = useSelect(); const tvStyles = selectStyles.trigger({ isDisabled: isDisabledProp || isDisabled, }); return ; }); // -------------------------------------------------- const SelectValue = forwardRef< SelectPrimitivesTypes.ValueRef, SelectValueProps >(({ className, ...props }, ref) => { const tvStyles = selectStyles.value({ className }); return ; }); // -------------------------------------------------- const SelectPortal = ({ className, children, ...props }: SelectPortalProps) => { const animationSettingsContext = useAnimationSettings(); const animationContext = useSelectAnimation(); const tvStyles = selectStyles.portal({ className }); return ( {children} ); }; // -------------------------------------------------- const SelectOverlay = forwardRef< SelectPrimitivesTypes.OverlayRef, SelectOverlayProps >( ( { className, style, animation, isAnimatedStyleActive = true, ...props }, ref ) => { const { progress, isDragging, isGestureReleaseAnimationRunning } = useSelectAnimation(); const overlayClassName = selectStyles.overlay({ className, }); const { rContainerStyle } = usePopupOverlayAnimation({ progress, isDragging, isGestureReleaseAnimationRunning, animation, }); const overlayStyle = isAnimatedStyleActive ? [rContainerStyle, style] : style; return ( ); } ); // -------------------------------------------------- const SelectContentPopover = forwardRef< SelectPrimitivesTypes.ContentRef, SelectContentProps & { 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 } = useSelectAnimation(); const contentClassName = selectStyles.popoverContent({ className, }); const { rContainerStyle } = usePopupPopoverContentAnimation({ progress, placement, animation, }); const contentStyle = isAnimatedStyleActive ? [styleSheet.contentContainer, rContainerStyle, style] : [styleSheet.contentContainer, style]; return ( {children} ); } ); // -------------------------------------------------- const SelectContentBottomSheet = forwardRef< BottomSheet, SelectContentProps & { presentation: 'bottom-sheet' } >( ( { children, backgroundClassName, handleIndicatorClassName, contentContainerClassName, contentContainerProps, animation, animationConfigs, ...restProps }, ref ) => { const { onOpenChange } = useSelect(); const { selectState, progress } = useSelectAnimation(); const { isAnimationDisabledValue } = useBottomSheetContentAnimation({ animation, }); const { animatedIndex } = usePopupBottomSheetContentAnimation({ progress, componentState: selectState, }); 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 SelectContentDialog = forwardRef< SelectPrimitivesTypes.ContentRef, SelectContentProps & { presentation: 'dialog' } >( ( { classNames, style, children, onLayout, animation, isSwipeable = true, isAnimatedStyleActive = true, ...props }, ref ) => { const { onOpenChange } = useSelect(); const { progress, isDragging, isGestureReleaseAnimationRunning, selectState, } = useSelectAnimation(); const { wrapper, content } = selectStyles.dialogContent(); const wrapperStyles = wrapper({ className: classNames?.wrapper }); const contentClassName = content({ className: classNames?.content }); const { contentY, contentHeight, panGesture, rDragContainerStyle, rContainerStyle, } = usePopupDialogContentAnimation({ progress, isDragging, isGestureReleaseAnimationRunning, dialogState: selectState, onOpenChange, animation, isSwipeable, }); const contentStyle = isAnimatedStyleActive ? [styleSheet.contentContainer, rContainerStyle, style] : [styleSheet.contentContainer, style]; const handleLayout = useCallback( (event: LayoutChangeEvent) => { contentY.set(event.nativeEvent.layout.y); contentHeight.set(event.nativeEvent.layout.height); onLayout?.(event); }, [contentY, contentHeight, onLayout] ); return ( {children} ); } ); // -------------------------------------------------- const SelectContent = forwardRef< SelectPrimitivesTypes.ContentRef | BottomSheet, SelectContentProps >((props, ref) => { const presentation = props.presentation || 'popover'; if (presentation === 'bottom-sheet') { return ( } {...(props as SelectContentBottomSheetProps)} /> ); } if (presentation === 'dialog') { return ( } {...(props as SelectContentDialogProps)} /> ); } return ( } {...(props as SelectContentPopoverProps)} /> ); }); // -------------------------------------------------- const SelectClose = forwardRef< SelectPrimitivesTypes.CloseRef, SelectCloseProps >(({ className, children, iconProps, hitSlop = 12, ...props }, ref) => { const themeColorMuted = useThemeColor('muted'); const tvStyles = selectStyles.close({ className }); return ( {children || ( )} ); }); // -------------------------------------------------- const SelectItem = forwardRef( ( { children, className, disabled = false, value: itemValue, label, ...props }, ref ) => { const { value } = useSelect(); const isSelected = value?.value === itemValue; const isDisabled = disabled ?? false; const tvStyles = selectStyles.item({ className }); const renderProps: SelectItemRenderProps = { isSelected, value: itemValue, isDisabled, }; const content = typeof children === 'function' ? children(renderProps) : (children ?? ( <> )); return ( {content} ); } ); // -------------------------------------------------- const SelectItemLabel = forwardRef< SelectPrimitivesTypes.ItemLabelRef, SelectItemLabelProps >(({ className, ...props }, ref) => { const { label } = useSelectItem(); const tvStyles = selectStyles.itemLabel({ className }); return ( {label} ); }); // -------------------------------------------------- const SelectItemDescription = forwardRef( ({ className, ...props }, ref) => { const tvStyles = selectStyles.itemDescription({ className, }); return ( ); } ); // -------------------------------------------------- const SelectItemIndicator = forwardRef< SelectPrimitivesTypes.ItemIndicatorRef, SelectItemIndicatorProps >(({ className, children, iconProps, ...props }, ref) => { const themeColorAccent = useThemeColor('accent'); const iconSize = iconProps?.size ?? 16; const iconColor = iconProps?.color ?? themeColorAccent; const tvStyles = selectStyles.itemIndicator({ className }); return ( {children || } ); }); // -------------------------------------------------- const SelectListLabel = forwardRef< SelectPrimitivesTypes.GroupLabelRef, SelectListLabelProps >(({ className, ...props }, ref) => { const tvStyles = selectStyles.listLabel({ className, }); return ( ); }); // -------------------------------------------------- SelectRoot.displayName = DISPLAY_NAME.ROOT; SelectTrigger.displayName = DISPLAY_NAME.TRIGGER; SelectPortal.displayName = DISPLAY_NAME.PORTAL; SelectOverlay.displayName = DISPLAY_NAME.OVERLAY; SelectContent.displayName = DISPLAY_NAME.CONTENT; SelectClose.displayName = DISPLAY_NAME.CLOSE; SelectItemDescription.displayName = DISPLAY_NAME.ITEM_DESCRIPTION; SelectValue.displayName = DISPLAY_NAME.VALUE; SelectItem.displayName = DISPLAY_NAME.ITEM; SelectItemLabel.displayName = DISPLAY_NAME.ITEM_LABEL; SelectItemIndicator.displayName = DISPLAY_NAME.ITEM_INDICATOR; SelectListLabel.displayName = DISPLAY_NAME.LIST_LABEL; /** * Compound Select component with sub-components * * @component Select - Main container that manages open/close state, positioning, * value selection and provides context to child components. Handles placement, alignment, and collision detection. * * @component Select.Trigger - Clickable element that toggles the select visibility. * Wraps any child element with press handlers. * * @component Select.Value - Displays the selected value or placeholder text. * Automatically updates when selection changes. * * @component Select.Portal - Renders select content in a portal layer above other content. * Ensures proper stacking and positioning. * * @component Select.Overlay - Optional background overlay. Can be transparent or * semi-transparent to capture outside clicks. * * @component Select.Content - Container for select content with three presentation modes: * popover (default floating with positioning and collision detection), bottom sheet modal, or dialog modal. * Supports custom animations. * * @component Select.Item - Selectable option item. Handles selection state and press events. * * @component Select.ItemLabel - Displays the label text for an item. * * @component Select.ItemIndicator - Optional indicator shown for selected items. * * @component Select.ListLabel - Label for the list of items. * * @component Select.Close - Close button that dismisses the select when pressed. * Renders a default X icon if no children provided. * * @component Select.ItemDescription - Optional description text for items with muted styling. * * Props flow from Select to sub-components via context (placement, align, offset, value, etc.). * The select automatically positions itself relative to the trigger element. * * @see Full documentation: https://heroui.com/components/select */ const Select = Object.assign(SelectRoot, { Trigger: SelectTrigger, Value: SelectValue, Portal: SelectPortal, Overlay: SelectOverlay, Content: SelectContent, Item: SelectItem, ItemLabel: SelectItemLabel, ItemDescription: SelectItemDescription, ItemIndicator: SelectItemIndicator, ListLabel: SelectListLabel, Close: SelectClose, }); export { useSelect, useSelectAnimation, useSelectItem }; export default Select;