import { ReactNode, useEffect, useRef, useState } from 'react';
import {
Animated,
Easing,
Keyboard,
Modal,
Platform,
Pressable,
StyleProp,
View,
ViewStyle,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useColors } from '../lib/theme';
/**
* Universal-Bottom-Sheet für Forms mit TextInput.
*
* Pattern (verifiziert auf PostCommentsSheet + EditMailAccountSheet):
*
* 1. Outer-Animated.View hat animated `height` (JS-driver) — Sheet WÄCHST
* bei Tastatur-Open um genau die Tastatur-Höhe.
* 2. Inner-Animated.View hat `transform: translateY` (Native-driver) —
* Slide-In/Out smooth. Driver-Mix-Trennung verhindert
* "Style property 'height' is not supported by native animated module"-Crash.
* 3. iOS: `paddingBottom: keyboardHeight` shifted Form innerhalb des
* gewachsenen Sheets über die Tastatur. Android: `windowSoftInputMode=adjustResize`
* im Manifest schrumpft das Window selbst.
* 4. Flex-Spacer drückt `children` (Form) automatisch an den Sheet-Bottom-Edge —
* sitzt direkt über der Tastatur ohne Gap.
*
* Anti-Pattern (siehe `docs/internal/RECOVERY_LOG_2026-05-10.md` §7.2):
* - `useKeyboardAnimation()` aus `react-native-keyboard-controller` liefert
* in iOS-Modals keine Höhe (separate UIWindow). Hier: plain RN
* `Keyboard.addListener` für die Höhe.
* - `Animated.subtract`/`marginBottom: keyboardHeight` mischen JS+Native-Driver
* auf demselben View → Bouncing oder Crash.
*
* Usage:
* ```tsx
* }
* >
*
*
*
*
*
* ```
*/
export interface KeyboardAwareSheetProps {
visible: boolean;
onClose: () => void;
/** Sheet-Höhe wenn Tastatur zu. Eng auf Inhalt zuschneiden — typisch 220-340px. */
collapsedHeight: number;
/** Optionaler Header (Cancel/Title-Row). Rendert direkt unter dem Drag-Handle. */
header?: ReactNode;
/** Form-Inhalt. Wird per Flex-Spacer an den Sheet-Bottom gedrückt — sitzt
* damit direkt über der Tastatur sobald die offen ist. */
children: ReactNode;
/** Default true — Tap auf Backdrop schließt das Sheet. */
dismissOnBackdrop?: boolean;
/** Default true — kleiner Drag-Handle ganz oben am Sheet. */
showDragHandle?: boolean;
/** Default true — fügt unten eine Safe-Area-Spacer-Höhe ein (insets.bottom). */
showSafeAreaSpacer?: boolean;
/** Default true — interner Flex-Spacer drückt children zum Sheet-Bottom.
* Auf false setzen wenn der Inhalt seine eigene Scroll-/Flex-Logik hat
* (z.B. ScrollView mit Provider-Grid, Listen). */
pushChildrenToBottom?: boolean;
/** Border-Radius oben. Default 20. */
topRadius?: number;
/** Optional zusätzlicher Style für den Sheet-Container. */
containerStyle?: StyleProp;
}
export function KeyboardAwareSheet({
visible,
onClose,
collapsedHeight,
header,
children,
dismissOnBackdrop = true,
showDragHandle = true,
showSafeAreaSpacer = true,
pushChildrenToBottom = true,
topRadius = 20,
containerStyle,
}: KeyboardAwareSheetProps) {
const colors = useColors();
const insets = useSafeAreaInsets();
const slideY = useRef(new Animated.Value(collapsedHeight)).current;
const backdropOpacity = useRef(new Animated.Value(0)).current;
const sheetHeight = useRef(new Animated.Value(collapsedHeight)).current;
const [keyboardHeight, setKeyboardHeight] = useState(0);
// Slide-In + Backdrop-Fade bei `visible=true`
useEffect(() => {
if (visible) {
slideY.setValue(collapsedHeight);
backdropOpacity.setValue(0);
Animated.parallel([
Animated.timing(slideY, {
toValue: 0,
duration: 280,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}),
Animated.timing(backdropOpacity, {
toValue: 1,
duration: 220,
useNativeDriver: true,
}),
]).start();
}
}, [visible, slideY, backdropOpacity, collapsedHeight]);
// Sheet-Höhe wächst/schrumpft mit Tastatur
useEffect(() => {
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const showSub = Keyboard.addListener(showEvent, (e) => {
const h = e.endCoordinates.height;
setKeyboardHeight(h);
Animated.timing(sheetHeight, {
toValue: collapsedHeight + h,
duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
});
const hideSub = Keyboard.addListener(hideEvent, (e) => {
setKeyboardHeight(0);
Animated.timing(sheetHeight, {
toValue: collapsedHeight,
duration: Platform.OS === 'ios' ? (e?.duration ?? 250) : 220,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
});
return () => {
showSub.remove();
hideSub.remove();
};
}, [sheetHeight, collapsedHeight]);
return (
{/* Backdrop */}
{dismissOnBackdrop && }
{/* Outer: animated height (JS-driver) */}
{/* Inner: animated transform (Native-driver). Driver-Mix vermeiden
durch zwei verschachtelte Animated.Views. */}
{showDragHandle && (
)}
{header}
{pushChildrenToBottom ? (
<>
{/* Flex-Spacer drückt children an den Sheet-Bottom */}
{children}
>
) : (
{children}
)}
{showSafeAreaSpacer && }
);
}