import React, { useCallback } from "react"; import { FlatList, ScrollView, View } from "react-native"; import { Text } from "@/components/common/Text"; import { useScaledTVSizes } from "@/constants/TVSizes"; import { useScaledTVTypography } from "@/constants/TVTypography"; interface TVHorizontalListProps { /** Data items to render */ data: T[]; /** Unique key extractor */ keyExtractor: (item: T, index: number) => string; /** Render function for each item */ renderItem: (info: { item: T; index: number }) => React.ReactElement | null; /** Optional section title */ title?: string; /** Text to show when data array is empty */ emptyText?: string; /** Whether to use FlatList (for large/infinite lists) or ScrollView (for small lists) */ useFlatList?: boolean; /** Called when end is reached (only for FlatList) */ onEndReached?: () => void; /** Ref for the scroll view */ scrollViewRef?: React.RefObject | null>; /** Footer component (only for FlatList) */ ListFooterComponent?: React.ReactElement | null; /** Whether this is the first section (for initial focus) */ isFirstSection?: boolean; /** Loading state */ isLoading?: boolean; /** Skeleton item count when loading */ skeletonCount?: number; /** Skeleton render function */ renderSkeleton?: () => React.ReactElement; /** * Custom horizontal padding (overrides default sizes.padding.scale). * Use this when the list needs to extend beyond its parent's padding. * The list will use negative margin to extend beyond the parent, * then add this padding inside to align content properly. */ horizontalPadding?: number; } /** * TVHorizontalList - A unified horizontal list component for TV. * * Provides consistent spacing and layout for horizontal lists: * - Uses `sizes.gaps.item` (24px default) for gap between items * - Uses `sizes.padding.scale` (20px default) for padding to accommodate focus scale * - Supports both ScrollView (small lists) and FlatList (large/infinite lists) */ export function TVHorizontalList({ data, keyExtractor, renderItem, title, emptyText, useFlatList = false, onEndReached, scrollViewRef, ListFooterComponent, isLoading = false, skeletonCount = 5, renderSkeleton, horizontalPadding, }: TVHorizontalListProps) { const sizes = useScaledTVSizes(); const typography = useScaledTVTypography(); // Use custom horizontal padding if provided, otherwise use default scale padding const effectiveHorizontalPadding = horizontalPadding ?? sizes.padding.scale; // Apply negative margin when using custom padding to extend beyond parent const marginHorizontal = horizontalPadding ? -horizontalPadding : 0; // Wrap renderItem to add consistent gap const renderItemWithGap = useCallback( ({ item, index }: { item: T; index: number }) => { const isLast = index === data.length - 1; return ( {renderItem({ item, index })} ); }, [data.length, renderItem, sizes.gaps.item], ); // Empty state if (!isLoading && data.length === 0 && emptyText) { return ( {title && ( {title} )} {emptyText} ); } // Loading state if (isLoading && renderSkeleton) { return ( {title && ( {title} )} {Array.from({ length: skeletonCount }).map((_, i) => ( {renderSkeleton()} ))} ); } const contentContainerStyle = { paddingHorizontal: effectiveHorizontalPadding, paddingVertical: sizes.padding.scale, }; const listStyle = { overflow: "visible" as const, marginHorizontal, }; return ( {title && ( {title} )} {useFlatList ? ( >} horizontal data={data} keyExtractor={keyExtractor} renderItem={renderItemWithGap} showsHorizontalScrollIndicator={false} removeClippedSubviews={false} style={listStyle} contentContainerStyle={contentContainerStyle} onEndReached={onEndReached} onEndReachedThreshold={0.5} initialNumToRender={5} maxToRenderPerBatch={3} windowSize={5} maintainVisibleContentPosition={{ minIndexForVisible: 0 }} ListFooterComponent={ListFooterComponent} /> ) : ( } horizontal showsHorizontalScrollIndicator={false} style={listStyle} contentContainerStyle={contentContainerStyle} > {data.map((item, index) => ( {renderItem({ item, index })} ))} {ListFooterComponent} )} ); }