miinventario-mobile-v2/src/components/ui/AnimatedList.tsx
rckrdmrd eb718a95aa Sincronización desde miinventario/apps/mobile - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:29:24 -06:00

155 lines
3.5 KiB
TypeScript

import React, { useCallback } from 'react';
import { FlatList, FlatListProps, ViewStyle, RefreshControl } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withDelay,
withSpring,
FadeIn,
SlideInRight,
Layout,
} from 'react-native-reanimated';
import { useTheme } from '../../theme/ThemeContext';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
interface AnimatedListProps<T> extends Omit<FlatListProps<T>, 'renderItem'> {
renderItem: (info: { item: T; index: number }) => React.ReactElement;
/** Delay base entre items en ms */
staggerDelay?: number;
/** Tipo de animación de entrada */
animationType?: 'fade' | 'slide' | 'spring';
/** Mostrar animación al refrescar */
animateOnRefresh?: boolean;
/** Callback de refresh */
onRefresh?: () => Promise<void> | void;
/** Está refrescando */
isRefreshing?: boolean;
}
/**
* Item animado wrapper
*/
function AnimatedItem({
children,
index,
staggerDelay,
animationType,
}: {
children: React.ReactNode;
index: number;
staggerDelay: number;
animationType: 'fade' | 'slide' | 'spring';
}) {
const delay = Math.min(index * staggerDelay, 500); // Cap máximo de delay
const entering = (() => {
switch (animationType) {
case 'slide':
return SlideInRight.delay(delay).duration(300);
case 'spring':
return FadeIn.delay(delay).springify();
case 'fade':
default:
return FadeIn.delay(delay).duration(300);
}
})();
return (
<Animated.View entering={entering} layout={Layout.springify()}>
{children}
</Animated.View>
);
}
/**
* FlatList con animaciones de entrada staggered
*/
export function AnimatedList<T>({
data,
renderItem,
staggerDelay = 50,
animationType = 'fade',
animateOnRefresh = true,
onRefresh,
isRefreshing = false,
...props
}: AnimatedListProps<T>) {
const { colors } = useTheme();
const [key, setKey] = React.useState(0);
const handleRefresh = useCallback(async () => {
if (onRefresh) {
await onRefresh();
if (animateOnRefresh) {
setKey((prev) => prev + 1);
}
}
}, [onRefresh, animateOnRefresh]);
const animatedRenderItem = useCallback(
({ item, index }: { item: T; index: number }) => (
<AnimatedItem
index={index}
staggerDelay={staggerDelay}
animationType={animationType}
>
{renderItem({ item, index })}
</AnimatedItem>
),
[renderItem, staggerDelay, animationType]
);
return (
<FlatList
key={key}
data={data}
renderItem={animatedRenderItem}
refreshControl={
onRefresh ? (
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={colors.primary}
colors={[colors.primary]}
/>
) : undefined
}
{...(props as any)}
/>
);
}
/**
* Hook para crear estilos animados de item de lista
*/
export function useListItemEntering(index: number, baseDelay = 50) {
const delay = Math.min(index * baseDelay, 500);
return FadeIn.delay(delay).duration(300);
}
/**
* Componente para animar un item individual
*/
export function AnimatedListItem({
children,
index,
baseDelay = 50,
style,
}: {
children: React.ReactNode;
index: number;
baseDelay?: number;
style?: ViewStyle;
}) {
const entering = useListItemEntering(index, baseDelay);
return (
<Animated.View entering={entering} style={style}>
{children}
</Animated.View>
);
}