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',
},
});