import React, { useContext, useEffect, useMemo } from 'react'; import { Pressable, StyleSheet, View, useWindowDimensions, type StyleProp, type ViewStyle, } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { Extrapolate, interpolate, useAnimatedStyle, useDerivedValue, useSharedValue, withDecay, type SharedValue, } from 'react-native-reanimated'; import { ContextualContext } from './ContextualContext'; export interface ContextualRootViewProps

{ offsetTop?: number; offsetBottom?: number; offsetRight?: number; background?: React.ReactNode; view: React.ReactNode; viewLayout: { x: number; y: number; width: number; height: number }; viewStyle: StyleProp; Menu: React.ComponentType

; menuProps: P; menuSize: { width: number; height: number }; anim: SharedValue; scroll: SharedValue; scaleAnim: SharedValue; } export function ContextualRootView

( props: ContextualRootViewProps

) { const { offsetTop: offsetTopProps, offsetBottom: offsetBottomProps, offsetRight: offsetRightProps, background, view, viewLayout, viewStyle, Menu, menuProps, menuSize, anim, scroll, scaleAnim, } = props; const offsetTop = offsetTopProps ?? 0; const offsetBottom = offsetBottomProps ?? 0; const offsetRight = offsetRightProps ?? 0; const window = useWindowDimensions(); const context = useContext(ContextualContext); const { closeContext } = context; const maxScroll = useSharedValue(0); const topAnim = useSharedValue(viewLayout.y); const leftAnim = useSharedValue(viewLayout.x); useEffect(() => { const topSafe = offsetTop ?? 0; const bottomSafe = offsetBottom ?? 0; const safeHeight = window.height - topSafe - bottomSafe; const fullHeight = viewLayout.height + menuSize.height; if (fullHeight > safeHeight) { maxScroll.value = fullHeight - safeHeight; } }, [ maxScroll, menuSize.height, offsetBottom, offsetTop, viewLayout.height, window.height, ]); const topStart = useDerivedValue(() => { const topSafe = offsetTop ?? 0; const bottomSafe = offsetBottom ?? 0; let topResult = topAnim.value; const isTopOverflow = topResult < topSafe; if (isTopOverflow) { topResult = topSafe; } const fullHeight = viewLayout.height + menuSize.height; const isBottomOverflow = topResult + fullHeight > window.height - bottomSafe; if (isBottomOverflow) { const delta = window.height - bottomSafe - topResult - fullHeight; topResult = topResult + delta; } return topResult; }, [offsetTop, offsetBottom, viewLayout]); const menuWidth = useSharedValue(0); useDerivedValue(() => { 'worklet'; topAnim.value = interpolate( anim.value, [0, 1], [viewLayout.y, topStart.value], Extrapolate.CLAMP ); }, []); const gesture = useMemo( () => Gesture.Pan() .onChange((e) => { const newTop = scroll.value + e.changeY; scroll.value = Math.min(maxScroll.value, Math.max(0, newTop)); }) .onEnd((e) => { scroll.value = withDecay({ velocity: e.velocityY, clamp: [0, maxScroll.value], }); }), [scroll, maxScroll] ); const viewAnimStyle = useAnimatedStyle( () => ({ position: 'absolute', left: viewLayout.x, top: topAnim.value + scroll.value, transform: [{ scale: scaleAnim.value }], }), [scaleAnim, topAnim, leftAnim, scroll] ); const menuStyle = useAnimatedStyle( () => ({ transform: [ { translateX: Math.min( window.width - leftAnim.value - menuWidth.value - offsetRight, 0 ), }, ], }), [menuWidth, leftAnim, window.width, offsetRight] ); return ( {background} {view} { menuWidth.value = e.nativeEvent.layout.width; }} >

); } const style = StyleSheet.create({ inset0: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, });