import React, { createContext, useCallback, useContext, useMemo, useState, type ReactElement, type ReactNode, } from 'react'; import { Platform, type NativeScrollEvent, type NativeSyntheticEvent, type RefreshControlProps, type ViewStyle, } from 'react-native'; export type SduiChromeScrollInsetProps = { contentInset?: { top: number; bottom: number }; contentOffset?: { x: number; y: number }; automaticallyAdjustContentInsets?: boolean; scrollIndicatorInsets?: { top: number; bottom: number }; }; export type SduiChromeScrollInsets = { top: number; bottom: number; headerHeight: number; tabBarHeight: number; }; export type SduiChromeScrollConfig = { scrollInsetProps: SduiChromeScrollInsetProps; contentContainerStyle: ViewStyle; chromeInsets: SduiChromeScrollInsets; onScroll?: (event: NativeSyntheticEvent) => void; /** Pull-to-refresh (Caçar e outras listas full-bleed SDUI). */ refreshControl?: ReactElement; /** Extra ScrollView props (ex.: touch handlers do pull web). */ scrollViewProps?: Record; }; export type SduiChromeScrollOverlayContribution = { /** Altura reservada abaixo do AppHeader (ex.: seletor + filtros do inbox). */ extraTopInset?: number; onScroll?: (event: NativeSyntheticEvent) => void; }; type OverlayRegistry = { register: (id: string, contribution: SduiChromeScrollOverlayContribution) => () => void; }; const SduiChromeScrollContext = createContext(null); const SduiChromeScrollOverlayRegistryContext = createContext(null); function mergeChromeScrollConfig( base: SduiChromeScrollConfig, overlays: Map, ): SduiChromeScrollConfig { let extraTop = 0; const scrollHandlers: ((event: NativeSyntheticEvent) => void)[] = []; if (base.onScroll) scrollHandlers.push(base.onScroll); for (const contribution of overlays.values()) { if (typeof contribution.extraTopInset === 'number' && contribution.extraTopInset > 0) { extraTop = Math.max(extraTop, contribution.extraTopInset); } if (contribution.onScroll) scrollHandlers.push(contribution.onScroll); } if (extraTop <= 0 && scrollHandlers.length <= (base.onScroll ? 1 : 0)) { return base; } const contentContainerStyle: ViewStyle = { ...base.contentContainerStyle }; let scrollInsetProps: SduiChromeScrollInsetProps = { ...base.scrollInsetProps }; if (extraTop > 0) { // Paridade com buildSduiChromeScrollConfig(extraTopInset): iOS reserva header via contentInset. const headerTop = base.chromeInsets.top; const insetTop = headerTop + extraTop; contentContainerStyle.paddingTop = Platform.OS === 'ios' ? extraTop : insetTop; if (Platform.OS === 'ios') { scrollInsetProps = { contentInset: { top: insetTop, bottom: base.chromeInsets.bottom, }, contentOffset: { x: 0, y: -insetTop }, automaticallyAdjustContentInsets: false, scrollIndicatorInsets: { top: insetTop, bottom: base.chromeInsets.bottom, }, }; } } return { ...base, contentContainerStyle, scrollInsetProps, onScroll: scrollHandlers.length > 0 ? (event) => { for (const handler of scrollHandlers) handler(event); } : undefined, }; } export function SduiChromeScrollProvider({ value, children, }: { value: SduiChromeScrollConfig; children: ReactNode; }) { const [overlays, setOverlays] = useState( () => new Map(), ); const register = useCallback((id: string, contribution: SduiChromeScrollOverlayContribution) => { setOverlays((prev) => { const next = new Map(prev); next.set(id, contribution); return next; }); return () => { setOverlays((prev) => { if (!prev.has(id)) return prev; const next = new Map(prev); next.delete(id); return next; }); }; }, []); const registry = useMemo(() => ({ register }), [register]); const merged = useMemo(() => mergeChromeScrollConfig(value, overlays), [overlays, value]); return ( {children} ); } export function useSduiChromeScroll(): SduiChromeScrollConfig | null { return useContext(SduiChromeScrollContext); } /** * Hybrids de overlay (ex.: inbox_hub_chrome) registram inset/onScroll sem especializar o shell. */ export function useSduiChromeScrollOverlayRegistry(): OverlayRegistry | null { return useContext(SduiChromeScrollOverlayRegistryContext); }