import type BottomSheet from '@gorhom/bottom-sheet'; import type { BottomSheetProps, BottomSheetBackgroundProps as GorhomBottomSheetBackgroundProps, } from '@gorhom/bottom-sheet'; import { forwardRef, useCallback, useEffect, useMemo, useState, type FC, } from 'react'; import type { StyleProp, ViewStyle } from 'react-native'; import { StyleSheet, View } from 'react-native'; import type { SharedValue } from 'react-native-reanimated'; import { ReduceMotion, useAnimatedReaction } from 'react-native-reanimated'; import { scheduleOnRN } from 'react-native-worklets'; import { withUniwind } from 'uniwind'; import { useBottomSheetContentAnimation } from '../../../components/bottom-sheet/bottom-sheet.animation'; import { DISPLAY_NAME as BOTTOM_SHEET_DISPLAY_NAME } from '../../../components/bottom-sheet/bottom-sheet.constants'; import { bottomSheetClassNames } from '../../../components/bottom-sheet/bottom-sheet.styles'; import type { BottomSheetBackgroundProps } from '../../../components/bottom-sheet/bottom-sheet.types'; import GorhomBottomSheetPackage from '../../../optional/gorhom-bottom-sheet'; import { BottomSheetIsDraggingProvider } from '../contexts'; import { useBottomSheetGestureHandlers } from '../hooks'; import { usePopupBottomSheetContentAnimation } from '../hooks/use-popup-bottom-sheet-content-animation'; import type { ViewRef } from '../types'; import type { BaseBottomSheetContentProps } from '../types/bottom-sheet'; import { BottomSheetContentContainer } from './bottom-sheet-content-container'; import { ThemeBackground } from './theme-background'; const StyledBottomSheet = withUniwind(GorhomBottomSheetPackage?.default); /** * Generic absolute-fill background container rendered inside the sheet * background surface, clipped to the sheet's top radius. 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. Exposed as `BottomSheet.Background`; * use it inside a custom gorhom `backgroundComponent` to customize the * default layer. */ export const BottomSheetBackground = forwardRef< ViewRef, BottomSheetBackgroundProps >(({ className, ...props }, ref) => { const backgroundClassName = bottomSheetClassNames.background({ className }); return ( ); }); BottomSheetBackground.displayName = BOTTOM_SHEET_DISPLAY_NAME.BACKGROUND; /** * Default gorhom `backgroundComponent`. Renders the sheet background surface * (styles arrive via gorhom's merged `style` prop, including the * `backgroundClassName`-derived styles) with the theme-aware background * layer inside. * * The accessibility props mirror gorhom's own `BottomSheetBackground` so * replacing it does not strip the sheet's screen reader identity. */ const BottomSheetDefaultBackground: FC = ({ style, pointerEvents, }) => { return ( ); }; /** * Props for the reusable BottomSheetContent component */ export interface BottomSheetContentProps extends BaseBottomSheetContentProps, Partial { /** * Whether the bottom sheet is open */ isOpen: boolean; /** * Animation progress shared value (0=idle, 1=open, 2=close) */ progress: SharedValue; /** * Whether the bottom sheet is dragging */ isDragging: SharedValue; /** * Callback when the bottom sheet open state changes */ onOpenChange: (open: boolean) => void; /** * Initial index of the bottom sheet */ index?: number; /** * Additional style for the background */ backgroundStyle?: StyleProp; } /** * Reusable BottomSheetContent component * * This component provides a reusable bottom sheet content wrapper used across * Popover, Select, and other components when using bottom-sheet presentation. * It handles animation coordination, styling, and gesture handling. * * @example * ```tsx * * {children} * * ``` */ export const BottomSheetContent = forwardRef< BottomSheet, BottomSheetContentProps >( ( { children, index: initialIndex, backgroundClassName, handleIndicatorClassName, contentContainerClassName: contentContainerClassNameProp, contentContainerProps, animation, animationConfigs, backgroundStyle, isOpen, progress, isDragging, onOpenChange, ...restProps }, ref ) => { const { isAnimationDisabledValue } = useBottomSheetContentAnimation({ animation, }); /** * The sheet stays mounted for its whole lifetime, so a dismissed one is * still rendered — parked below the container. iOS resizes the container * a few frames before React Native reports the new size, and in that gap * the sheet is painted at the offset the previous size parked it at, which * flashes it on screen mid-rotation. Taking a dismissed sheet out of the * paint entirely closes that gap: it is only ever hidden while it is * already meant to be invisible. */ const [isDismissed, setIsDismissed] = useState(!isOpen); useEffect(() => { if (isOpen) { setIsDismissed(false); } }, [isOpen]); useAnimatedReaction( () => progress.get(), (value) => { /** * 0 and 2 are the two resting points of a closed sheet. Compare with * a small epsilon: on Android a closed sheet can sit slightly above * index `-1` when gorhom's initial `window.height` is shorter than * the container, so progress never lands on exactly `0` or `2` and * the peek would stay painted. */ if (!isOpen && (value <= 0.01 || value >= 1.99)) { scheduleOnRN(setIsDismissed, true); } } ); /** * Reveal the sheet only when our open state says it should be seen. * Gorhom can fire `onAnimate` to a snap index `>= 0` on its own — dynamic * sizing and the `window` vs container height mismatch both do this while * `isOpen` is still `false` — and treating that as a real open would * un-hide a closed sheet so its handle peeks at the bottom of the screen. */ const onAnimate = restProps.onAnimate; const handleAnimate = useCallback( ( fromIndex: number, toIndex: number, fromPosition: number, toPosition: number ) => { if (toIndex >= 0 && isOpen) { setIsDismissed(false); } onAnimate?.(fromIndex, toIndex, fromPosition, toPosition); }, [isOpen, onAnimate] ); const containerStyle = useMemo( () => [ restProps.containerStyle, isDismissed && !isOpen && styles.dismissedContainer, ], [restProps.containerStyle, isDismissed, isOpen] ); /** * Theme-aware background layer support: render the default background * component unless the caller provides their own `backgroundComponent`. * An explicit `null` is preserved — gorhom treats it as "render no * background surface at all", so it must not fall back to the default. */ const backgroundComponent = restProps.backgroundComponent === undefined ? BottomSheetDefaultBackground : restProps.backgroundComponent; const { animatedIndex, isClosingOnSwipe, isPanActivated } = usePopupBottomSheetContentAnimation({ progress, isDragging, }); const contentBackgroundClassName = bottomSheetClassNames.contentBackground({ className: backgroundClassName, }); const contentHandleIndicatorClassName = bottomSheetClassNames.contentHandleIndicator({ className: handleIndicatorClassName, }); const contentContainerClassName = bottomSheetClassNames.contentContainer({ className: contentContainerClassNameProp, }); const mergedAnimationConfigs = useMemo( () => ({ ...animationConfigs, reduceMotion: isAnimationDisabledValue ? ReduceMotion.Always : animationConfigs?.reduceMotion, }), [animationConfigs, isAnimationDisabledValue] ); return ( {children} ); } ); BottomSheetContent.displayName = 'HeroUINative.BottomSheetContent'; const styles = StyleSheet.create({ dismissedContainer: { opacity: 0, pointerEvents: 'none', }, });