diff --git a/README.md b/README.md index b632348..afb8d3a 100644 --- a/README.md +++ b/README.md @@ -438,6 +438,7 @@ Supports: - `scrollId?: string` for multi-scroll scenarios - `headerOffsetStrategy?: 'padding' | 'margin' | 'top' | 'translate' | 'none'` - `ensureScrollableContentMinHeight?: boolean` + Experimental. Defaults to `false`. `padding` is the default and recommended option. `top` and `translate` also add bottom compensation internally so the end of the content remains reachable. @@ -450,6 +451,7 @@ Supports: - `scrollId?: string` for multi-scroll scenarios - `headerOffsetStrategy?: 'padding' | 'margin' | 'top' | 'translate' | 'none'` - `ensureScrollableContentMinHeight?: boolean` + Experimental. Defaults to `false`. #### `createHeaderMotionScrollable(Component, options?)` @@ -461,6 +463,7 @@ Returned components support: - `scrollId?: string` - `headerOffsetStrategy?: 'padding' | 'margin' | 'top' | 'translate' | 'none'` - `ensureScrollableContentMinHeight?: boolean` + Experimental. Defaults to `false`. Use: diff --git a/example/src/app/index.tsx b/example/src/app/index.tsx index bb2d53a..9390750 100644 --- a/example/src/app/index.tsx +++ b/example/src/app/index.tsx @@ -99,7 +99,7 @@ const SECTIONS: ShowcaseSection[] = [ ], }, { - title: 'Content & Refs', + title: 'Layout edge cases', data: [ { title: 'Short Content (min height)', diff --git a/example/src/app/scroll-manager.tsx b/example/src/app/scroll-manager.tsx index 56ba090..3cf8f6b 100644 --- a/example/src/app/scroll-manager.tsx +++ b/example/src/app/scroll-manager.tsx @@ -28,14 +28,14 @@ export default function Screen() { {( scrollViewProps, - { originalHeaderHeight, minHeightContentContainerStyle } + { originalHeaderHeight, contentContainerMinHeight } ) => ( {content} diff --git a/example/src/app/short-content-no-min-height.tsx b/example/src/app/short-content-no-min-height.tsx index 7bb5742..30cc2ea 100644 --- a/example/src/app/short-content-no-min-height.tsx +++ b/example/src/app/short-content-no-min-height.tsx @@ -24,9 +24,7 @@ export default function Screen() { /> )} - - {content} - + {content} ); } diff --git a/example/src/app/short-content.tsx b/example/src/app/short-content.tsx index 471623b..9778c0b 100644 --- a/example/src/app/short-content.tsx +++ b/example/src/app/short-content.tsx @@ -24,7 +24,9 @@ export default function Screen() { /> )} - {content} + + {content} + ); } diff --git a/src/components/HeaderMotion.tsx b/src/components/HeaderMotion.tsx index c5b4cc7..0aaa65a 100644 --- a/src/components/HeaderMotion.tsx +++ b/src/components/HeaderMotion.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useEffect, useMemo } from 'react'; +import { useCallback, useRef, useEffect, useMemo, useState } from 'react'; import { Extrapolation, interpolate, @@ -111,7 +111,7 @@ function HeaderMotionContextProvider({ children, }: HeaderMotionProps) { const dynamicMeasurement = useSharedValue(undefined); - const originalHeaderHeight = useSharedValue(0); + const [originalHeaderHeight, setOriginalHeaderHeight] = useState(0); const progressThresholdValue = useSharedValue( typeof progressThreshold === 'number' ? progressThreshold : Infinity ); @@ -131,11 +131,11 @@ function HeaderMotionContextProvider({ } dynamicMeasurement.set(measured); - progressThresholdValue.set( + const nextThreshold = typeof progressThreshold === 'number' ? progressThreshold - : progressThreshold(measured) - ); + : progressThreshold(measured); + progressThresholdValue.set(nextThreshold); }, [ measureDynamicMode, @@ -153,21 +153,17 @@ function HeaderMotionContextProvider({ } const measured = dynamicMeasurement.get(); - progressThresholdValue.set( - measured === undefined ? Infinity : progressThreshold(measured) - ); + const nextThreshold = + measured === undefined ? Infinity : progressThreshold(measured); + progressThresholdValue.set(nextThreshold); }, [progressThreshold, dynamicMeasurement, progressThresholdValue]); const measureTotalHeight = useCallback( (e) => { const measuredValue = e.nativeEvent.layout.height; - if (originalHeaderHeight.get() === measuredValue) { - return; - } - - originalHeaderHeight.set(measuredValue); + setOriginalHeaderHeight(measuredValue); }, - [originalHeaderHeight] + [setOriginalHeaderHeight] ); const scrollValues = useSharedValue({ diff --git a/src/components/ScrollManager.tsx b/src/components/ScrollManager.tsx index 8115d14..a45ee8b 100644 --- a/src/components/ScrollManager.tsx +++ b/src/components/ScrollManager.tsx @@ -24,11 +24,14 @@ export interface HeaderMotionScrollManagerProps< } /** - * ScrollManager component that provides scroll tracking functionality for custom scroll implementations. Uses {@link useScrollManager} under the hood. + * ScrollManager component that provides scroll tracking functionality for + * custom scroll implementations. Uses {@link useScrollManager} under the hood. * Must be used within a HeaderMotion component. * * This is useful when you need to use a scroll component that isn't directly supported * (like a custom scroll view or third-party list components). + * If you would rather compose the same functionality in a hook-based API, + * use {@link useScrollManager} directly. * * @example * ```tsx diff --git a/src/components/__tests__/createHeaderMotionScrollable.test.tsx b/src/components/__tests__/createHeaderMotionScrollable.test.tsx index 29cc546..3cbc28c 100644 --- a/src/components/__tests__/createHeaderMotionScrollable.test.tsx +++ b/src/components/__tests__/createHeaderMotionScrollable.test.tsx @@ -8,6 +8,8 @@ jest.mock('react', () => { ...ReactActual, useMemo: (factory: () => unknown) => factory(), useCallback: any>(callback: T) => callback, + useRef: (value: T) => ({ current: value }), + useLayoutEffect: (effect: () => void) => effect(), forwardRef: (render: any) => { const Forwarded = (props: any) => render(props, null); Forwarded.render = render; @@ -103,7 +105,7 @@ function createScrollManagerResult() { }, headerMotionContext: { originalHeaderHeight: 48, - minHeightContentContainerStyle: { minHeight: 320 }, + contentContainerMinHeight: 320, }, }; } diff --git a/src/components/createHeaderMotionScrollable.tsx b/src/components/createHeaderMotionScrollable.tsx index 75f7e62..6cff026 100644 --- a/src/components/createHeaderMotionScrollable.tsx +++ b/src/components/createHeaderMotionScrollable.tsx @@ -1,13 +1,15 @@ import { forwardRef, useCallback, + useLayoutEffect, useMemo, + useRef, type ComponentRef, type ReactElement, type ReactNode, type Ref, } from 'react'; -import type { ScrollViewProps } from 'react-native'; +import type { LayoutChangeEvent, ScrollViewProps } from 'react-native'; import Animated, { type AnimatedProps, type AnimatedRef, @@ -74,18 +76,18 @@ export function createHeaderMotionScrollable< )})`, } = options || {}; - const AnimatedScrollable = (isComponentAnimated - ? ScrollableComponent - : Animated.createAnimatedComponent( - ScrollableComponent as never - )) as unknown as ScrollableImplementationComponent; + const AnimatedScrollable = ( + isComponentAnimated + ? ScrollableComponent + : Animated.createAnimatedComponent(ScrollableComponent) + ) as ScrollableImplementationComponent; function HeaderMotionScrollable(props: ScrollableRuntimeProps) { const { scrollId, animatedRef, headerOffsetStrategy, - ensureScrollableContentMinHeight = true, + ensureScrollableContentMinHeight = false, contentContainerStyle, refreshControl, refreshing, @@ -114,31 +116,39 @@ export function createHeaderMotionScrollable< onMomentumScrollBegin, onMomentumScrollEnd, animatedRef, + ensureScrollableContentMinHeight, } ); const { onScroll: managedOnScroll, + onLayout: managedOnLayout, refreshControl: managedRefreshControl, ref, ...scrollViewProps } = scrollableProps; - const { originalHeaderHeight, minHeightContentContainerStyle } = + const { originalHeaderHeight, contentContainerMinHeight } = headerMotionContext; + const userOnLayoutRef = useRef(rest.onLayout as UserOnLayout); + useLayoutEffect(() => { + userOnLayoutRef.current = rest.onLayout as UserOnLayout; + }); + const managedContentContainerStyle = useMemo( () => [ - ensureScrollableContentMinHeight - ? minHeightContentContainerStyle + ensureScrollableContentMinHeight && + contentContainerMinHeight !== undefined + ? { minHeight: contentContainerMinHeight } : undefined, resolveHeaderOffsetStyle(originalHeaderHeight, headerOffsetStrategy), contentContainerStyle, ], [ contentContainerStyle, + contentContainerMinHeight, ensureScrollableContentMinHeight, headerOffsetStrategy, - minHeightContentContainerStyle, originalHeaderHeight, ] ); @@ -147,6 +157,14 @@ export function createHeaderMotionScrollable< refreshControl: managedRefreshControl, }; + const handleLayout = useCallback( + (e: LayoutChangeEvent) => { + managedOnLayout?.(e); + userOnLayoutRef.current?.(e); + }, + [managedOnLayout] + ); + const contentContainerProps = useContentContainerProps({ children: rest.children, mode: contentContainerMode, @@ -160,6 +178,7 @@ export function createHeaderMotionScrollable< {...refreshControlProps} {...contentContainerProps} ref={ref} + onLayout={handleLayout} onScroll={managedOnScroll} /> ); @@ -222,6 +241,8 @@ function getDisplayName(ScrollableComponent: { ); } +type UserOnLayout = ScrollViewProps['onLayout'] | undefined; + // TODO: From here below Codex did some absolute TypeScript magic but it seems to work // Having limited time, I can't spend more on adjusting this to make it less convoluted // But what matters is that it seems that for the user the types work very well diff --git a/src/context.ts b/src/context.ts index e7a26af..d63727a 100644 --- a/src/context.ts +++ b/src/context.ts @@ -18,7 +18,7 @@ interface HeaderMotionContextType { scrollValues: SharedValue; activeScrollId: SharedValue | undefined; progressThreshold: SharedValue; - originalHeaderHeight: SharedValue; + originalHeaderHeight: number; scrollToRef: React.RefObject; } diff --git a/src/hooks/useMotionProgress.ts b/src/hooks/useMotionProgress.ts index 5c3b975..0ac704e 100644 --- a/src/hooks/useMotionProgress.ts +++ b/src/hooks/useMotionProgress.ts @@ -24,7 +24,7 @@ import type { MotionProgress } from '../types'; * const translateY = interpolate( * progress.value, * [0, 1], - * [0, -progressThreshold], + * [0, -progressThreshold.get()], * Extrapolation.CLAMP, * ) * return { transform: [{ translateY }] } diff --git a/src/hooks/useScrollManager.ts b/src/hooks/useScrollManager.ts index 92866d5..d3b97a1 100644 --- a/src/hooks/useScrollManager.ts +++ b/src/hooks/useScrollManager.ts @@ -1,18 +1,24 @@ -import { useContext, useCallback, useEffect } from 'react'; +import { + useContext, + useCallback, + useEffect, + useState, + type ContextType, +} from 'react'; import { cancelAnimation, - measure, scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, - useAnimatedStyle, + useSharedValue, type AnimatedRef, type ScrollHandler, } from 'react-native-reanimated'; -import { RuntimeKind, scheduleOnUI } from 'react-native-worklets'; +import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets'; import { HeaderMotionContext } from '../context'; import type { ScrollManagerConfig, ScrollHandlerContext } from '../types'; +import type { LayoutChangeEvent } from 'react-native'; import { resolveRefreshControl, DEFAULT_SCROLL_ID, @@ -29,62 +35,25 @@ import { const SCROLL_TOLERANCE = 0.5; -/** - * Hook that manages scroll tracking and synchronization for header animations. - * Returns props to apply to scrollable components and additional values that help with adjusting styling of the scrollables to header's dimensions. - * - * This hook handles: - * - Scroll position tracking - * - Synchronization between multiple scroll views (when using multiple scroll IDs) - * - Content container minimum height calculations for cases where one of the tracked scrollables does not take enough space to reach the progress threshold/ - * - * Must be used within a HeaderMotion component. - * - * @param scrollId - Optional unique identifier for the related scrollable. - * Use when you have multiple scrollables (e.g., in tabs). - * @param options - Optional configuration object. - * @param options.animatedRef - Optional animated ref to use instead of creating one internally. - * Useful when you need access to the scroll view ref from outside. - * @returns Configuration object containing: - * - `scrollableProps`: Props to apply to scrollable component (onScroll, ref) - * - `headerMotionContext`: Header context values (originalHeaderHeight, minHeightContentContainerStyle) - * - * @throws Error if used outside of a HeaderMotion component - * - * @example - * ```tsx - * function CustomScrollComponent() { - * const { scrollableProps, headerMotionContext } = useScrollManager('myScroll'); - * - * return ( - * - * - * Content - * - * - * ); - * } - * ``` - */ -export interface UseScrollManagerOptions - extends Omit, - ConsumerScrollEventHandlers { - /** - * Optional animated ref to use instead of creating one internally. - * Useful when you need access to the scroll view ref from outside. - */ - animatedRef?: AnimatedRef; - /** - * Optional refresh progress offset override. - * When provided, it takes precedence over the automatic offset based on header height. - */ - progressViewOffset?: ResolveRefreshControlOptions['progressViewOffset']; +type ScrollManagerContextValue = NonNullable< + ContextType +>; + +interface MinHeightOptions { + enabled: boolean; } -export function useScrollManager( - scrollId?: string, - options?: UseScrollManagerOptions -): ScrollManagerConfig { +interface SynchronizationOptions { + animatedRef: AnimatedRef; + id: string; +} + +interface ScrollHandlersOptions { + consumerHandlers: ConsumerScrollEventHandlers; + id: string; +} + +function useScrollManagerContext(): ScrollManagerContextValue { const ctxValue = useContext(HeaderMotionContext); if (!ctxValue) { throw new Error( @@ -92,32 +61,71 @@ export function useScrollManager( ); } + return ctxValue; +} + +function useScrollManagerContentMinHeight({ enabled }: MinHeightOptions) { + const { progressThreshold } = useScrollManagerContext(); + const preservedScrollContainerHeight = useSharedValue(0); + const [contentContainerMinHeight, setContentContainerMinHeight] = useState< + number | undefined + >(undefined); + + const handleLayout = useCallback( + (e: LayoutChangeEvent) => { + if (!enabled) { + return; + } + + const nextHeight = e.nativeEvent.layout.height; + scheduleOnUI((height: number) => { + 'worklet'; + preservedScrollContainerHeight.set(height); + const nextMinHeight = height + progressThreshold.get(); + scheduleOnRN(setContentContainerMinHeight, nextMinHeight); + }, nextHeight); + }, + [enabled, preservedScrollContainerHeight, progressThreshold] + ); + + useAnimatedReaction( + () => progressThreshold.get(), + (threshold, previousThreshold) => { + if ( + !enabled || + previousThreshold === null || + previousThreshold === threshold + ) { + return; + } + + const currentHeight = preservedScrollContainerHeight.get(); + if (currentHeight <= 0) { + return; + } + + const nextMinHeight = currentHeight + threshold; + scheduleOnRN(setContentContainerMinHeight, nextMinHeight); + } + ); + + return { + contentContainerMinHeight, + handleLayout: enabled ? handleLayout : undefined, + }; +} + +function useScrollManagerSynchronization({ + animatedRef, + id, +}: SynchronizationOptions) { const { - scrollValues, - progress, activeScrollId, + progress, progressThreshold, - originalHeaderHeight, scrollToRef, - headerPanMomentumOffset, - } = ctxValue; - const id = scrollId ?? DEFAULT_SCROLL_ID; - - const localRef = useAnimatedRef(); - const animatedRef = options?.animatedRef ?? localRef; - const refreshControl = options?.refreshControl; - const refreshing = options?.refreshing; - const onRefresh = options?.onRefresh; - const { onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd } = - useConsumerScrollHandlers({ - onScroll: options?.onScroll, - onScrollBeginDrag: options?.onScrollBeginDrag, - onScrollEndDrag: options?.onScrollEndDrag, - onMomentumScrollBegin: options?.onMomentumScrollBegin, - onMomentumScrollEnd: options?.onMomentumScrollEnd, - }); - const progressViewOffset = - options?.progressViewOffset ?? originalHeaderHeight; + scrollValues, + } = useScrollManagerContext(); useAnimatedReaction( () => activeScrollId?.get(), @@ -147,13 +155,11 @@ export function useScrollManager( }); }, id); }; - }, [scrollValues, id]); + }, [id, scrollValues]); useAnimatedReaction( () => progress.value, (newProgress, oldProgress) => { - // FUTURE: If really needed for, can use other scroll handlers to only do this either on scroll end or between scroll end and momentum end in onScroll (keep context in shared value) - // Only sync inactive scroll views when we have multiple tabs being tracked const currentActiveScrollId = activeScrollId?.get(); if ( !currentActiveScrollId || @@ -188,6 +194,20 @@ export function useScrollManager( } } ); +} + +function useScrollManagerHandlers({ + consumerHandlers, + id, +}: ScrollHandlersOptions) { + const { + activeScrollId, + headerPanMomentumOffset, + progressThreshold, + scrollValues, + } = useScrollManagerContext(); + const { onScroll, onBeginDrag, onEndDrag, onMomentumBegin, onMomentumEnd } = + useConsumerScrollHandlers(consumerHandlers); const handleScroll = useCallback>( (e, ctx) => { @@ -221,13 +241,6 @@ export function useScrollManager( const oldMin = scrollValue.min; const isCollapsed = oldCurrent >= oldMin + threshold - 0.001; - // When the header is fully collapsed and the user is scrolled past the - // threshold, progress is mathematically guaranteed to stay at 1: - // min = newCurrent - threshold → (newCurrent - min) / threshold = 1 - // In this case we update the values directly via .get() instead of - // .modify(), which avoids triggering the reactive cascade (progress - // re-derivation, animated reactions, animated styles). The values are - // still updated in-place for tab synchronization correctness. if (isCollapsed && newCurrent >= threshold) { scrollValue.current = newCurrent; scrollValue.min = newCurrent - threshold; @@ -248,7 +261,7 @@ export function useScrollManager( return value; }); }, - [scrollValues, id, activeScrollId, progressThreshold, onScroll] + [activeScrollId, id, onScroll, progressThreshold, scrollValues] ); const handleBeginDrag = useCallback>( @@ -266,30 +279,119 @@ export function useScrollManager( [headerPanMomentumOffset, onBeginDrag] ); - const animatedOnScroll = useAnimatedScrollHandler({ + return useAnimatedScrollHandler({ onBeginDrag: handleBeginDrag, onScroll: handleScroll, onEndDrag, onMomentumBegin, onMomentumEnd, }); +} +export interface UseScrollManagerOptions + extends Omit, + ConsumerScrollEventHandlers { + /** + * Optional animated ref to use instead of creating one internally. + * Useful when you need access to the scroll view ref from outside. + */ + animatedRef?: AnimatedRef; + /** + * Optional refresh progress offset override. + * When provided, it takes precedence over the automatic offset based on header height. + */ + progressViewOffset?: ResolveRefreshControlOptions['progressViewOffset']; + /** + * Experimental: opt-in fallback for short content that cannot scroll far enough + * to fully collapse the header. + */ + ensureScrollableContentMinHeight?: boolean; +} - const minHeightContentContainerStyle = useAnimatedStyle(() => { - const threshold = progressThreshold.get(); +/** + * Manages scroll tracking, synchronization, and scrollable wiring for a + * collapsible header. + * + * Use this hook inside `HeaderMotion` when integrating a custom scrollable + * component instead of one of the built-in `HeaderMotion.*` wrappers. + * If you prefer the same functionality from a render function instead of + * calling a hook directly, use {@link HeaderMotionScrollManager}. + * + * Responsibilities: + * - tracks the active scroll position used to drive header progress + * - synchronizes inactive scrollables in multi-scroll setups + * - wires refresh-related props through the library's refresh-control helper + * - optionally computes a plain `contentContainerMinHeight` fallback for short + * content when `ensureScrollableContentMinHeight` is enabled + * + * @param scrollId Optional unique identifier for the related scrollable. + * Use this when tracking multiple scrollables, for example inside tabs. + * @param options Optional configuration for refs, refresh handling, consumer + * scroll callbacks, and the experimental short-content min-height fallback. + * @returns Object containing: + * - `scrollableProps`: props to spread onto the scrollable (`ref`, managed + * `onScroll`, optional `onLayout`, and resolved `refreshControl`) + * - `headerMotionContext`: layout values for offsetting the content container + * (`originalHeaderHeight` and optional `contentContainerMinHeight`) + * + * @throws Error when used outside of a `HeaderMotion` provider. + * + * @example + * ```tsx + * function CustomScrollComponent() { + * const { scrollableProps, headerMotionContext } = useScrollManager('myScroll'); + * + * return ( + * + * + * Content + * + * + * ); + * } + * ``` + */ +export function useScrollManager( + scrollId?: string, + options?: UseScrollManagerOptions +): ScrollManagerConfig { + const { originalHeaderHeight } = useScrollManagerContext(); + const id = scrollId ?? DEFAULT_SCROLL_ID; - if (globalThis.__RUNTIME_KIND === RuntimeKind.ReactNative) { - return {}; - } + const ensureScrollableContentMinHeight = + options?.ensureScrollableContentMinHeight ?? false; + const refreshControl = options?.refreshControl; + const refreshing = options?.refreshing; + const onRefresh = options?.onRefresh; + const progressViewOffset = + options?.progressViewOffset ?? originalHeaderHeight; - const measurement = measure(animatedRef); + const localRef = useAnimatedRef(); + const animatedRef = options?.animatedRef ?? localRef; - if (!measurement) { - return {}; - } + const { contentContainerMinHeight, handleLayout } = + useScrollManagerContentMinHeight({ + enabled: ensureScrollableContentMinHeight, + }); - return { - minHeight: measurement.height + threshold, - }; + useScrollManagerSynchronization({ + id, + animatedRef, + }); + + const animatedOnScroll = useScrollManagerHandlers({ + id, + consumerHandlers: { + onScroll: options?.onScroll, + onScrollBeginDrag: options?.onScrollBeginDrag, + onScrollEndDrag: options?.onScrollEndDrag, + onMomentumScrollBegin: options?.onMomentumScrollBegin, + onMomentumScrollEnd: options?.onMomentumScrollEnd, + }, }); const resolvedRefreshControl = resolveRefreshControl({ @@ -301,12 +403,13 @@ export function useScrollManager( const scrollableProps = { onScroll: useScrollHandlerComposition(animatedOnScroll, options?.onScroll), + onLayout: handleLayout, ref: animatedRef, refreshControl: resolvedRefreshControl, }; const headerMotionContext = { originalHeaderHeight, - minHeightContentContainerStyle, + contentContainerMinHeight, }; return { scrollableProps, headerMotionContext }; diff --git a/src/index.ts b/src/index.ts index 7c1277f..b655847 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,9 @@ type HeaderMotionComponent = { * Use to pass props to the header components in React Navigation / Expo Router, which cannot access HeaderMotion's context and `useMotionProgress` otherwise. */ Header: typeof HeaderMotionHeader; - /** Component for custom scroll implementations */ + /** Component for custom scroll implementations. + * Use when you want render-prop composition instead of calling {@link useScrollManager} directly. + */ ScrollManager: typeof HeaderMotionScrollManager; /** Animated ScrollView component with header motion integration */ ScrollView: typeof HeaderMotionScrollView; diff --git a/src/types.ts b/src/types.ts index 2264bd4..b0c0b86 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,7 +25,9 @@ export interface HeaderMotionOffsetProps { * Ensures the content container gets a minimum height large enough for short * content to still scroll far enough to drive the header to its collapsed state. * - * @default true + * Experimental: this relies on extra layout measurement and may be refined in a future release. + * + * @default false */ ensureScrollableContentMinHeight?: boolean; } @@ -78,16 +80,12 @@ export interface AnimatedHeaderBaseMotionProps { } export interface ScrollManagerHeaderMotionContext { - originalHeaderHeight: SharedValue; - minHeightContentContainerStyle: - | {} - | { - minHeight: number; - }; + originalHeaderHeight: number; + contentContainerMinHeight?: number; } export interface ScrollManagerConfig { - scrollableProps: Pick & { + scrollableProps: Pick & { refreshControl?: ReactElement; ref: AnimatedRef; };