import React, { forwardRef, useCallback, useContext, useRef, type ReactNode } from 'react'; import { Dimensions, View, ViewStyle } from 'react-native'; import { SafeAreaInsetsContext, type EdgeInsets } from 'react-native-safe-area-context'; import { useResolveClassNames } from 'uniwind'; import { BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView, BottomSheetView, BottomSheetBackdrop, BottomSheetFooter as GorhomBottomSheetFooter, type BottomSheetBackdropProps, type BottomSheetFooterProps as GorhomBottomSheetFooterProps, type BottomSheetModalProps as GorhomBottomSheetModalProps, } from '@gorhom/bottom-sheet'; import { cn } from '@cdx-ui/utils'; import { ArrowBack, Close } from '@cdx-ui/icons'; import { Heading } from '../Heading'; import { IconButton } from '../IconButton'; import { Text } from '../Text'; import { bottomSheetBackgroundClassName, bottomSheetContentVariants, bottomSheetFooterVariants, bottomSheetHeaderVariants, bottomSheetModalContainerClassName, } from './styles'; const ZERO_INSETS: EdgeInsets = { top: 0, bottom: 0, left: 0, right: 0 }; function useSafeInsets(): EdgeInsets { return useContext(SafeAreaInsetsContext) ?? ZERO_INSETS; } /** Ref type for BottomSheet; forwards the @gorhom modal ref (present, dismiss, snapToIndex, minimize, restore, etc.). */ export type BottomSheetModalRef = React.ComponentRef; export interface BottomSheetHeaderProps { title?: string; subtitle?: string; children?: ReactNode; className?: string; onClose?: () => void; closeAccessibilityLabel?: string; onBack?: () => void; backAccessibilityLabel?: string; } function BottomSheetHeaderComponent( { title, subtitle, children, className, onClose, closeAccessibilityLabel, onBack, backAccessibilityLabel = 'Go back', }: BottomSheetHeaderProps, ref: React.Ref, ) { const hasTitle = title !== undefined && title !== ''; const hasContent = hasTitle || !!children || !!onBack; const computedClassName = cn(bottomSheetHeaderVariants({ hasTitle: hasContent }), className); return ( {children ?? (hasTitle ? ( {title} {subtitle ? ( {subtitle} ) : null} ) : null)} ); } BottomSheetHeaderComponent.displayName = 'BottomSheet.Header'; const BottomSheetHeader = forwardRef(BottomSheetHeaderComponent); BottomSheetHeader.displayName = 'BottomSheet.Header'; // ============================================================================= // BACKBUTTON // ============================================================================= interface BottomSheetBackButtonProps { onPress?: () => void; className?: string; accessibilityLabel?: string; } function BottomSheetBackButton({ onPress, className, accessibilityLabel = 'Go back', }: Readonly) { return ( ); } BottomSheetBackButton.displayName = 'BottomSheet.BackButton'; // ============================================================================= // CLOSEBUTTON // ============================================================================= interface BottomSheetCloseButtonProps { onPress?: () => void; className?: string; accessibilityLabel?: string; } function BottomSheetCloseButton({ onPress, className, accessibilityLabel = 'Close', }: Readonly) { return ( ); } BottomSheetCloseButton.displayName = 'BottomSheet.CloseButton'; // ============================================================================= // CONTENT // ============================================================================= export interface BottomSheetContentProps { children?: ReactNode; className?: string; hasSafeAreaInset?: boolean; style?: ViewStyle; } const BottomSheetContent = forwardRef( ({ children, className, hasSafeAreaInset, style }, ref) => { const insets = useSafeInsets(); const computedClassName = cn(bottomSheetContentVariants(), className); return ( {children} ); }, ); BottomSheetContent.displayName = 'BottomSheet.Content'; // ============================================================================= // FOOTER // ============================================================================= export interface BottomSheetFooterProps { children?: ReactNode; className?: string; style?: ViewStyle; } const BottomSheetFooter = forwardRef( ({ children, className, style }, ref) => { const insets = useSafeInsets(); const computedClassName = cn(bottomSheetFooterVariants(), className); return ( {children} ); }, ); BottomSheetFooter.displayName = 'BottomSheet.Footer'; // ============================================================================= // MODAL WRAPPER (WITH STYLES) // ============================================================================= export interface BottomSheetModalProps extends GorhomBottomSheetModalProps { /** Show the drag handle indicator. When false, `handleComponent` is set to null. @default true */ handleVisible?: boolean; /** Footer content rendered via gorhom's BottomSheetFooter for proper animated positioning and safe-area handling. */ footer?: ReactNode; } const MAX_DYNAMIC_CONTENT_SIZE = Dimensions.get('window').height * 0.9; const CustomBackdrop = (props: BottomSheetBackdropProps) => { return ( ); }; const CustomBackground = (props: React.ComponentPropsWithoutRef) => ( ); const BottomSheetModalStyled = forwardRef< React.ComponentRef, BottomSheetModalProps >(({ handleVisible = true, footer, onDismiss, onChange, ...rest }, ref) => { const renderFooter = useCallback( (props: GorhomBottomSheetFooterProps) => ( {footer} ), [footer], ); const dismissedRef = useRef(false); const handleDismiss = useCallback(() => { if (dismissedRef.current) return; dismissedRef.current = true; onDismiss?.(); }, [onDismiss]); const handleChange = useCallback( (...args: Parameters>) => { dismissedRef.current = false; onChange?.(...args); }, [onChange], ); const sheetContainerStyle = useResolveClassNames(bottomSheetModalContainerClassName); const handleIndicatorStyle = useResolveClassNames( 'bg-surface-neutral-subtle w-9 h-[5px] rounded-full', ); return ( ); }); BottomSheetModalStyled.displayName = 'BottomSheet.Modal'; // ============================================================================= // SCROLLVIEW WRAPPER (with footer margin adjustment enabled by default) // ============================================================================= function BottomSheetScrollViewStyled({ enableFooterMarginAdjustment = true, children, ...rest }: React.ComponentPropsWithoutRef) { return ( {children} ); } BottomSheetScrollViewStyled.displayName = 'BottomSheet.ScrollView'; // ============================================================================= // VIEW WRAPPER (with footer margin adjustment enabled by default) // ============================================================================= function BottomSheetViewStyled({ enableFooterMarginAdjustment = true, children, ...rest }: React.ComponentPropsWithoutRef) { return ( {children} ); } BottomSheetViewStyled.displayName = 'BottomSheet.View'; // Display names for gorhom components exposed as BottomSheet.* interface WithDisplayName { displayName?: string; } (BottomSheetModalProvider as WithDisplayName).displayName = 'BottomSheet.Provider'; // ── Public exports ──────────────────────────────────────── interface BottomSheetCompound { Modal: typeof BottomSheetModalStyled; Header: typeof BottomSheetHeader; Content: typeof BottomSheetContent; Footer: typeof BottomSheetFooter; Provider: typeof BottomSheetModalProvider; ScrollView: typeof BottomSheetScrollViewStyled; View: typeof BottomSheetViewStyled; } export const BottomSheet = { Modal: BottomSheetModalStyled, Header: BottomSheetHeader, Content: BottomSheetContent, Footer: BottomSheetFooter, Provider: BottomSheetModalProvider, ScrollView: BottomSheetScrollViewStyled, View: BottomSheetViewStyled, } as BottomSheetCompound; // ── Hooks ──────────────────────────────────────────────── export function useBottomSheet() { const ref = useRef(null); return { ref, present: () => ref.current?.present(), dismiss: () => ref.current?.dismiss() }; }