import type BottomSheet from '@gorhom/bottom-sheet'; import { forwardRef, useLayoutEffect, useMemo, useRef } from 'react'; import type { GestureResponderEvent } from 'react-native'; import { StyleSheet, useWindowDimensions, View } from 'react-native'; import { GestureDetector } from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useThemeColor } from '../../helpers/external/hooks'; import { cn } from '../../helpers/external/utils'; import { BottomSheetContent, CheckIcon, ChevronDownIcon, FullWindowOverlay, HeroText, PortalGestureRoot, ThemeBackground, useHasDefaultThemeBackground, } from '../../helpers/internal/components'; import { AnimationSettingsProvider, useAnimationSettings, } from '../../helpers/internal/contexts'; import { usePopupDialogContentAnimation, usePopupOverlayAnimation, usePopupPopoverContentAnimation, usePopupRootAnimation, } from '../../helpers/internal/hooks'; import type { PressableRef, TextRef, ViewRef, } from '../../helpers/internal/types'; import * as SelectPrimitives from '../../primitives/select'; import * as SelectPrimitivesTypes from '../../primitives/select/select.types'; import { CloseButton } from '../close-button'; import { SelectAnimationProvider, useSelectAnimation, useSelectTriggerIndicatorAnimation, } from './select.animation'; import { DEFAULT_ALIGN_OFFSET, DEFAULT_ICON_SIZE, DEFAULT_INSETS, DEFAULT_OFFSET, DISPLAY_NAME, } from './select.constants'; import { selectClassNames, selectStyleSheet } from './select.styles'; import type { SelectCloseProps, SelectContentBackgroundProps, SelectContentBottomSheetProps, SelectContentDialogProps, SelectContentPopoverProps, SelectContentProps, SelectItemDescriptionProps, SelectItemIndicatorProps, SelectItemLabelProps, SelectItemProps, SelectItemRenderProps, SelectListLabelProps, SelectOverlayProps, SelectPortalProps, SelectRootProps, SelectTriggerBackgroundProps, SelectTriggerIndicatorProps, SelectTriggerProps, SelectValueProps, } from './select.types'; const AnimatedOverlay = Animated.createAnimatedComponent( SelectPrimitives.Overlay ); const AnimatedPopoverContent = Animated.createAnimatedComponent( SelectPrimitives.PopoverContent ); const AnimatedTriggerIndicator = Animated.createAnimatedComponent( SelectPrimitives.TriggerIndicator ); const useSelect = SelectPrimitives.useRootContext; const useSelectItem = SelectPrimitives.useItemContext; // -------------------------------------------------- function SelectRoot({ children, ref, isOpen, isDefaultOpen, onOpenChange, animation, ...props }: SelectRootProps & { ref?: React.Ref; }) { const { progress, isDragging, isGestureReleaseAnimationRunning, isAllAnimationsDisabled, } = usePopupRootAnimation({ animation, }); const animationContextValue = useMemo( () => ({ progress, isDragging, isGestureReleaseAnimationRunning, }), [progress, isDragging, isGestureReleaseAnimationRunning] ); const animationSettingsContextValue = useMemo( () => ({ isAllAnimationsDisabled, }), [isAllAnimationsDisabled] ); return ( {children} ); } // -------------------------------------------------- /** * Generic absolute-fill background container rendered behind the default * variant's trigger surface. With no `children`, the active library theme * decides the default content: `glass` renders a `GlassView` blur layer; * other themes render nothing. Pass `children` to host arbitrary content * (gradients, images) with the container's positioning and clipping applied. */ const SelectTriggerBackground = forwardRef< ViewRef, SelectTriggerBackgroundProps >(({ className, ...props }, ref) => { const triggerBackgroundClassName = selectClassNames.triggerBackground({ className, }); return ( ); }); // -------------------------------------------------- const SelectTrigger = forwardRef< SelectPrimitivesTypes.TriggerRef, SelectTriggerProps >( ( { variant = 'default', isDisabled: isDisabledProp, className, children, background, ...props }, ref ) => { const { isDisabled } = useSelect(); const hasDefaultThemeBackground = useHasDefaultThemeBackground(); const triggerClassName = selectClassNames.trigger({ variant, isDisabled: isDisabledProp || isDisabled, className, }); /** * Background layer rendered behind the trigger surface. * - `undefined`: theme-aware default for the default variant when the * active theme registers default background content * - custom node: replaces the default layer * - `null`: removes the layer * Skipped entirely when `asChild` is set — the Slot pattern requires a * single child element. */ const backgroundElement = props.asChild ? null : background !== undefined ? ( background ) : hasDefaultThemeBackground && variant === 'default' ? ( ) : null; return ( {backgroundElement == null ? ( children ) : ( <> {backgroundElement} {children} )} ); } ); // -------------------------------------------------- const SelectValue = forwardRef< SelectPrimitivesTypes.ValueRef, SelectValueProps >(({ className, ...props }, ref) => { const { value } = useSelect(); const isSelected = Array.isArray(value) ? value.length > 0 : Boolean(value?.value); const valueClassName = selectClassNames.value({ isSelected, className, }); return ( ); }); // -------------------------------------------------- const SelectTriggerIndicator = forwardRef( (props, ref) => { const { children, className, iconProps, animation, isAnimatedStyleActive = true, style, ...restProps } = props; const { isOpen } = useSelect(); const themeColorForeground = useThemeColor('foreground'); const triggerIndicatorClassName = selectClassNames.triggerIndicator({ className, }); const { rContainerStyle } = useSelectTriggerIndicatorAnimation({ animation, isOpen, }); const triggerIndicatorStyle = isAnimatedStyleActive ? [rContainerStyle, style] : style; return ( {children ?? ( )} ); } ); // -------------------------------------------------- const SelectPortal = ({ className, children, disableFullWindowOverlay = false, unstable_accessibilityContainerViewIsModal, ...props }: SelectPortalProps) => { const animationSettingsContext = useAnimationSettings(); const animationContext = useSelectAnimation(); const portalClassName = selectClassNames.portal({ className }); return ( {children} ); }; // -------------------------------------------------- const SelectOverlay = forwardRef< SelectPrimitivesTypes.OverlayRef, SelectOverlayProps >( ( { className, style, animation, isAnimatedStyleActive = true, ...props }, ref ) => { const { isOpen, presentation } = useSelect(); const { progress, isDragging, isGestureReleaseAnimationRunning } = useSelectAnimation(); const overlayClassName = selectClassNames.overlay({ className, }); const { rContainerStyle, entering, exiting } = usePopupOverlayAnimation({ progress: presentation !== 'popover' ? progress : undefined, isDragging: presentation === 'popover' ? isDragging : undefined, isGestureReleaseAnimationRunning: presentation === 'dialog' ? isGestureReleaseAnimationRunning : undefined, animation, }); const overlayStyle = isAnimatedStyleActive ? [rContainerStyle, style] : style; return ( ); } ); // -------------------------------------------------- /** * Generic absolute-fill background container rendered behind the select * content (shared by popover and dialog presentations). With no `children`, * the active library theme decides the default content: `glass` renders a * `GlassView` blur layer; other themes render nothing. Pass `children` to * host arbitrary content (gradients, images) with the container's * positioning and clipping applied. */ const SelectContentBackground = forwardRef< ViewRef, SelectContentBackgroundProps >(({ className, ...props }, ref) => { const contentBackgroundClassName = selectClassNames.contentBackground({ className, }); return ( ); }); // -------------------------------------------------- const SelectContentPopover = forwardRef< SelectPrimitivesTypes.ContentRef, SelectContentProps & { presentation?: 'popover' } >( ( { placement = 'bottom', align = 'center', avoidCollisions = true, offset = DEFAULT_OFFSET, alignOffset = DEFAULT_ALIGN_OFFSET, className, children, background, style, animation, ...props }, ref ) => { const { contentLayout } = useSelect(); const safeAreaInsets = useSafeAreaInsets(); const { height: screenHeight } = useWindowDimensions(); // Initially useRelativePosition returns { position: 'absolute', opacity: 0, top: dimensions.height } // So we need to wait for the content to be ready before showing it const isReady = Boolean(contentLayout?.y && contentLayout.y < screenHeight); 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 contentClassName = selectClassNames.content({ className, }); const { isDrivenEntering, rEnteringStyle, entering, exiting } = usePopupPopoverContentAnimation({ placement, offset, animation, isReady, }); /** * Background layer rendered behind the select content. `undefined` * falls back to the theme-aware default; `null` removes the layer. */ const backgroundElement = background === undefined ? : background; const contentChildren = ( <> {backgroundElement} {children} ); // Single-mount path: the content subtree is rendered once and animated in // via a shared value once it has been measured and positioned (`isReady`). if (isDrivenEntering) { return ( // The wrapper must fill the portal: Android only dispatches native // touches to children inside their parent's bounds, so a zero-height // wrapper breaks native scrolling (e.g. a `ScrollView`) in the content. {contentChildren} ); } // Fallback path: a custom entering Keyframe was provided, which must fire // on mount, so a hidden probe measures the content before the visible node // mounts and plays the Keyframe. return ( <> {isReady && ( {contentChildren} )} {children} ); } ); // -------------------------------------------------- const SelectContentBottomSheet = forwardRef< BottomSheet, SelectContentProps & { presentation: 'bottom-sheet' } >( ( { children, index: initialIndex, backgroundClassName, handleIndicatorClassName, contentContainerClassName: contentContainerClassNameProp, contentContainerProps, animation, animationConfigs, ...restProps }, ref ) => { const { isOpen, onOpenChange } = useSelect(); const { progress, isDragging } = useSelectAnimation(); return ( {children} ); } ); // -------------------------------------------------- const SelectContentDialog = forwardRef< SelectPrimitivesTypes.ContentRef, SelectContentProps & { presentation: 'dialog' } >( ( { classNames, styles, style, children, background, animation, isSwipeable = true, ...props }, ref ) => { const { isOpen, onOpenChange } = useSelect(); const { progress, isDragging, isGestureReleaseAnimationRunning } = useSelectAnimation(); const { wrapper, content } = selectClassNames.dialogContent(); const wrapperClassName = wrapper({ className: classNames?.wrapper }); const contentClassName = content({ className: classNames?.content }); const dragContainerRef = useRef(null); const { contentY, contentHeight, panGesture, rDragContainerStyle, entering, exiting, } = usePopupDialogContentAnimation({ progress, isDragging, isGestureReleaseAnimationRunning, isOpen, onOpenChange, animation, isSwipeable, }); useLayoutEffect(() => { dragContainerRef.current?.measure( (_x, _y, _width, height, _pageX, pageY) => { contentY.set(pageY); contentHeight.set(height); } ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /** * Background layer rendered behind the select content. `undefined` * falls back to the theme-aware default; `null` removes the layer. */ const backgroundElement = background === undefined ? : background; return ( {backgroundElement} {children} ); } ); // -------------------------------------------------- const SelectContent = forwardRef< SelectPrimitivesTypes.ContentRef | BottomSheet, SelectContentProps >((props, ref) => { const presentation = props.presentation || 'popover'; const { presentation: contextPresentation } = useSelect(); if (__DEV__) { if (presentation !== contextPresentation) { throw new Error( `Select.Content presentation prop ("${props.presentation}") does not match Select.Root presentation prop ("${contextPresentation}"). They must be the same.` ); } } if (presentation === 'bottom-sheet') { return ( } {...(props as SelectContentBottomSheetProps)} /> ); } if (presentation === 'dialog') { return ( } {...(props as SelectContentDialogProps)} /> ); } return ( } {...(props as SelectContentPopoverProps)} /> ); }); // -------------------------------------------------- const SelectClose = forwardRef((props, ref) => { const { onPress: onPressProp, ...restProps } = props; const { onOpenChange } = useSelect(); const onPress = (ev: GestureResponderEvent) => { onOpenChange(false); if (typeof onPressProp === 'function') { onPressProp(ev); } }; return ; }); // -------------------------------------------------- const SelectItem = forwardRef( ( { children, className, disabled = false, value: itemValue, label, ...props }, ref ) => { const { value } = useSelect(); const isSelected = Array.isArray(value) ? value.some((v) => v?.value === itemValue) : value?.value === itemValue; const isDisabled = disabled ?? false; const itemClassName = selectClassNames.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 itemLabelClassName = selectClassNames.itemLabel({ className }); return ( {label} ); }); // -------------------------------------------------- const SelectItemDescription = forwardRef( ({ className, ...props }, ref) => { const itemDescriptionClassName = selectClassNames.itemDescription({ className, }); return ( ); } ); // -------------------------------------------------- const SelectItemIndicator = forwardRef< SelectPrimitivesTypes.ItemIndicatorRef, SelectItemIndicatorProps >(({ className, children, iconProps, ...props }, ref) => { const themeColorAccentSoftForeground = useThemeColor( 'accent-soft-foreground' ); const iconSize = iconProps?.size ?? 16; const iconColor = iconProps?.color ?? themeColorAccentSoftForeground; const itemIndicatorClassName = selectClassNames.itemIndicator({ className }); return ( {children || } ); }); // -------------------------------------------------- const SelectListLabel = forwardRef< SelectPrimitivesTypes.GroupLabelRef, SelectListLabelProps >(({ className, ...props }, ref) => { const listLabelClassName = selectClassNames.listLabel({ className, }); return ( ); }); // -------------------------------------------------- SelectRoot.displayName = DISPLAY_NAME.ROOT; SelectTrigger.displayName = DISPLAY_NAME.TRIGGER; SelectTriggerBackground.displayName = DISPLAY_NAME.TRIGGER_BACKGROUND; SelectTriggerIndicator.displayName = DISPLAY_NAME.TRIGGER_INDICATOR; SelectValue.displayName = DISPLAY_NAME.VALUE; SelectPortal.displayName = DISPLAY_NAME.PORTAL; SelectOverlay.displayName = DISPLAY_NAME.OVERLAY; SelectContent.displayName = DISPLAY_NAME.CONTENT; SelectContentBackground.displayName = DISPLAY_NAME.CONTENT_BACKGROUND; SelectClose.displayName = DISPLAY_NAME.CLOSE; SelectItemDescription.displayName = DISPLAY_NAME.ITEM_DESCRIPTION; 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.TriggerBackground - Absolute-fill background container * behind the default variant's trigger surface. With no children, the active * library theme decides the content (glass theme renders a blur layer with a * surface-matched fallback). Accepts children to host custom content such as * gradients with the container's positioning and clipping applied. Replaceable * via the `background` prop on Select.Trigger. * * @component Select.TriggerIndicator - Optional visual indicator showing open/close state. * Defaults to an animated chevron icon that rotates based on select state. * Supports custom animation configuration. * * @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.ContentBackground - Absolute-fill background container behind * the select content (popover and dialog presentations). With no children, the * active library theme decides the content (glass theme renders a blur layer). * Accepts children to host custom content such as gradients with the container's * positioning and clipping applied. Replaceable via the `background` prop on * Select.Content. * * @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 for the select. * Can accept custom children or uses default close icon. * * @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/docs/native/components/select */ const Select = Object.assign(SelectRoot, { Trigger: SelectTrigger, /** @optional Theme-aware background container behind the trigger surface */ TriggerBackground: SelectTriggerBackground, /** @optional Visual indicator showing open/close state (defaults to chevron) */ TriggerIndicator: SelectTriggerIndicator, Value: SelectValue, Portal: SelectPortal, Overlay: SelectOverlay, Content: SelectContent, /** @optional Theme-aware background container behind the select content */ ContentBackground: SelectContentBackground, Item: SelectItem, ItemLabel: SelectItemLabel, ItemDescription: SelectItemDescription, ItemIndicator: SelectItemIndicator, ListLabel: SelectListLabel, Close: SelectClose, }); export { useSelect, useSelectAnimation, useSelectItem }; export default Select;