@souscheflabs/reanimated-flashlist 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +282 -0
  2. package/lib/AnimatedFlashList.d.ts +6 -0
  3. package/lib/AnimatedFlashList.d.ts.map +1 -0
  4. package/lib/AnimatedFlashList.js +207 -0
  5. package/lib/AnimatedFlashListItem.d.ts +33 -0
  6. package/lib/AnimatedFlashListItem.d.ts.map +1 -0
  7. package/lib/AnimatedFlashListItem.js +155 -0
  8. package/lib/__tests__/utils/test-utils.d.ts +82 -0
  9. package/lib/__tests__/utils/test-utils.d.ts.map +1 -0
  10. package/lib/__tests__/utils/test-utils.js +115 -0
  11. package/lib/constants/animations.d.ts +39 -0
  12. package/lib/constants/animations.d.ts.map +1 -0
  13. package/lib/constants/animations.js +100 -0
  14. package/lib/constants/drag.d.ts +11 -0
  15. package/lib/constants/drag.d.ts.map +1 -0
  16. package/lib/constants/drag.js +47 -0
  17. package/lib/constants/index.d.ts +3 -0
  18. package/lib/constants/index.d.ts.map +1 -0
  19. package/lib/constants/index.js +18 -0
  20. package/lib/contexts/DragStateContext.d.ts +73 -0
  21. package/lib/contexts/DragStateContext.d.ts.map +1 -0
  22. package/lib/contexts/DragStateContext.js +148 -0
  23. package/lib/contexts/ListAnimationContext.d.ts +104 -0
  24. package/lib/contexts/ListAnimationContext.d.ts.map +1 -0
  25. package/lib/contexts/ListAnimationContext.js +184 -0
  26. package/lib/contexts/index.d.ts +5 -0
  27. package/lib/contexts/index.d.ts.map +1 -0
  28. package/lib/contexts/index.js +10 -0
  29. package/lib/hooks/animations/index.d.ts +9 -0
  30. package/lib/hooks/animations/index.d.ts.map +1 -0
  31. package/lib/hooks/animations/index.js +13 -0
  32. package/lib/hooks/animations/useListEntryAnimation.d.ts +38 -0
  33. package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -0
  34. package/lib/hooks/animations/useListEntryAnimation.js +90 -0
  35. package/lib/hooks/animations/useListExitAnimation.d.ts +67 -0
  36. package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -0
  37. package/lib/hooks/animations/useListExitAnimation.js +146 -0
  38. package/lib/hooks/drag/index.d.ts +20 -0
  39. package/lib/hooks/drag/index.d.ts.map +1 -0
  40. package/lib/hooks/drag/index.js +26 -0
  41. package/lib/hooks/drag/useDragAnimatedStyle.d.ts +33 -0
  42. package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -0
  43. package/lib/hooks/drag/useDragAnimatedStyle.js +61 -0
  44. package/lib/hooks/drag/useDragGesture.d.ts +30 -0
  45. package/lib/hooks/drag/useDragGesture.d.ts.map +1 -0
  46. package/lib/hooks/drag/useDragGesture.js +189 -0
  47. package/lib/hooks/drag/useDragShift.d.ts +21 -0
  48. package/lib/hooks/drag/useDragShift.d.ts.map +1 -0
  49. package/lib/hooks/drag/useDragShift.js +85 -0
  50. package/lib/hooks/drag/useDropCompensation.d.ts +27 -0
  51. package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -0
  52. package/lib/hooks/drag/useDropCompensation.js +90 -0
  53. package/lib/hooks/index.d.ts +8 -0
  54. package/lib/hooks/index.d.ts.map +1 -0
  55. package/lib/hooks/index.js +18 -0
  56. package/lib/index.d.ts +42 -0
  57. package/lib/index.d.ts.map +1 -0
  58. package/lib/index.js +69 -0
  59. package/lib/types/animations.d.ts +71 -0
  60. package/lib/types/animations.d.ts.map +1 -0
  61. package/lib/types/animations.js +2 -0
  62. package/lib/types/drag.d.ts +94 -0
  63. package/lib/types/drag.d.ts.map +1 -0
  64. package/lib/types/drag.js +2 -0
  65. package/lib/types/index.d.ts +4 -0
  66. package/lib/types/index.d.ts.map +1 -0
  67. package/lib/types/index.js +19 -0
  68. package/lib/types/list.d.ts +136 -0
  69. package/lib/types/list.d.ts.map +1 -0
  70. package/lib/types/list.js +2 -0
  71. package/package.json +73 -0
  72. package/src/AnimatedFlashList.tsx +411 -0
  73. package/src/AnimatedFlashListItem.tsx +212 -0
  74. package/src/__tests__/components/AnimatedFlashList.test.tsx +365 -0
  75. package/src/__tests__/components/AnimatedFlashListItem.test.tsx +371 -0
  76. package/src/__tests__/contexts/DragStateContext.test.tsx +169 -0
  77. package/src/__tests__/contexts/ListAnimationContext.test.tsx +324 -0
  78. package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +118 -0
  79. package/src/__tests__/hooks/useDragGesture.test.tsx +169 -0
  80. package/src/__tests__/hooks/useDragShift.test.tsx +94 -0
  81. package/src/__tests__/hooks/useDropCompensation.test.tsx +182 -0
  82. package/src/__tests__/hooks/useListEntryAnimation.test.tsx +135 -0
  83. package/src/__tests__/hooks/useListExitAnimation.test.tsx +175 -0
  84. package/src/__tests__/utils/test-utils.tsx +159 -0
  85. package/src/constants/animations.ts +107 -0
  86. package/src/constants/drag.ts +51 -0
  87. package/src/constants/index.ts +2 -0
  88. package/src/contexts/DragStateContext.tsx +197 -0
  89. package/src/contexts/ListAnimationContext.tsx +302 -0
  90. package/src/contexts/index.ts +9 -0
  91. package/src/hooks/animations/index.ts +9 -0
  92. package/src/hooks/animations/useListEntryAnimation.ts +108 -0
  93. package/src/hooks/animations/useListExitAnimation.ts +197 -0
  94. package/src/hooks/drag/index.ts +20 -0
  95. package/src/hooks/drag/useDragAnimatedStyle.ts +80 -0
  96. package/src/hooks/drag/useDragGesture.ts +267 -0
  97. package/src/hooks/drag/useDragShift.ts +119 -0
  98. package/src/hooks/drag/useDropCompensation.ts +120 -0
  99. package/src/hooks/index.ts +16 -0
  100. package/src/index.ts +105 -0
  101. package/src/types/animations.ts +76 -0
  102. package/src/types/drag.ts +101 -0
  103. package/src/types/index.ts +3 -0
  104. package/src/types/list.ts +178 -0
@@ -0,0 +1,136 @@
1
+ import type { ReactElement, ComponentType, JSXElementConstructor } from 'react';
2
+ import type { ViewStyle } from 'react-native';
3
+ import type { FlashListProps } from '@shopify/flash-list';
4
+ import type { SharedValue } from 'react-native-reanimated';
5
+ import type { GestureType } from 'react-native-gesture-handler';
6
+ import type { DragConfig } from './drag';
7
+ import type { AnimationDirection, ExitAnimationPreset, ExitAnimationConfig, EntryAnimationConfig, HapticFeedbackType } from './animations';
8
+ /**
9
+ * Base item interface - consumers extend this
10
+ */
11
+ export interface AnimatedListItem {
12
+ id: string;
13
+ }
14
+ /**
15
+ * Props passed to drag handle wrapper
16
+ */
17
+ export interface DragHandleProps {
18
+ /** The pan gesture to attach */
19
+ gesture: GestureType;
20
+ /** Whether dragging is currently active */
21
+ isDragging: SharedValue<boolean>;
22
+ }
23
+ /**
24
+ * Render item info passed to renderItem callback
25
+ */
26
+ export interface AnimatedRenderItemInfo<T extends AnimatedListItem> {
27
+ /** The item data */
28
+ item: T;
29
+ /** Current index in list */
30
+ index: number;
31
+ /** Total number of items */
32
+ totalItems: number;
33
+ /** Combined animated style (drag + entry/exit) - apply to outer Animated.View */
34
+ animatedStyle: ViewStyle;
35
+ /** Drag handle props - spread onto GestureDetector wrapping your drag handle */
36
+ dragHandleProps: DragHandleProps | null;
37
+ /** Whether this item is currently being dragged */
38
+ isDragging: boolean;
39
+ /** Whether drag is enabled for this item */
40
+ isDragEnabled: boolean;
41
+ /** Trigger exit animation programmatically */
42
+ triggerExitAnimation: (direction: AnimationDirection, onComplete: () => void, preset?: ExitAnimationPreset) => void;
43
+ /** Reset exit animation state */
44
+ resetExitAnimation: () => void;
45
+ }
46
+ /**
47
+ * Configuration for the AnimatedFlashList
48
+ */
49
+ export interface AnimatedFlashListConfig {
50
+ /** Drag behavior configuration */
51
+ drag?: Partial<DragConfig>;
52
+ /** Exit animation configuration */
53
+ exitAnimation?: Partial<ExitAnimationConfig>;
54
+ /** Entry animation configuration */
55
+ entryAnimation?: Partial<EntryAnimationConfig>;
56
+ }
57
+ /**
58
+ * Props for AnimatedFlashList component
59
+ */
60
+ export interface AnimatedFlashListProps<T extends AnimatedListItem> extends Omit<FlashListProps<T>, 'data' | 'renderItem' | 'keyExtractor' | 'ref'> {
61
+ /** List data */
62
+ data: T[];
63
+ /** Key extractor function */
64
+ keyExtractor: (item: T, index: number) => string;
65
+ /**
66
+ * Render function for each item
67
+ * Receives item data and animation utilities
68
+ */
69
+ renderItem: (info: AnimatedRenderItemInfo<T>) => ReactElement;
70
+ /**
71
+ * Whether drag-to-reorder is enabled
72
+ * @default false
73
+ */
74
+ dragEnabled?: boolean;
75
+ /**
76
+ * Callback when an item is reordered
77
+ * @param itemId - ID of the moved item
78
+ * @param fromIndex - Original index
79
+ * @param toIndex - New index
80
+ */
81
+ onReorder?: (itemId: string, fromIndex: number, toIndex: number) => void;
82
+ /**
83
+ * Callback with neighbor-based reorder info (for fractional indexing)
84
+ * @param itemId - ID of the moved item
85
+ * @param afterItemId - ID of item that should come before (null if moving to start)
86
+ * @param beforeItemId - ID of item that should come after (null if moving to end)
87
+ */
88
+ onReorderByNeighbors?: (itemId: string, afterItemId: string | null, beforeItemId: string | null) => void;
89
+ /**
90
+ * Function to determine if an item can be dragged
91
+ * @default () => true when dragEnabled is true
92
+ */
93
+ canDrag?: (item: T, index: number) => boolean;
94
+ /**
95
+ * Optional haptic feedback callback
96
+ * Called during drag operations
97
+ */
98
+ onHapticFeedback?: (type: HapticFeedbackType) => void;
99
+ /**
100
+ * Configuration overrides
101
+ */
102
+ config?: AnimatedFlashListConfig;
103
+ /**
104
+ * Called when FlashList needs to prepare for layout animation
105
+ * (before items are added/removed)
106
+ */
107
+ onPrepareLayoutAnimation?: () => void;
108
+ /** Footer component */
109
+ ListFooterComponent?: ReactElement<unknown, string | JSXElementConstructor<unknown>> | ComponentType<unknown> | null;
110
+ /** Refresh callback */
111
+ onRefresh?: () => void | Promise<void>;
112
+ /** Whether currently refreshing */
113
+ refreshing?: boolean;
114
+ /** Pagination callback */
115
+ onEndReached?: () => void;
116
+ /** Pagination threshold */
117
+ onEndReachedThreshold?: number;
118
+ }
119
+ /**
120
+ * Ref handle for AnimatedFlashList
121
+ */
122
+ export interface AnimatedFlashListRef {
123
+ /**
124
+ * Prepare FlashList for layout animation before items are removed/added
125
+ */
126
+ prepareForLayoutAnimation: () => void;
127
+ /**
128
+ * Scroll to a specific offset
129
+ */
130
+ scrollToOffset: (offset: number, animated?: boolean) => void;
131
+ /**
132
+ * Scroll to a specific index
133
+ */
134
+ scrollToIndex: (index: number, animated?: boolean) => void;
135
+ }
136
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/types/list.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@souscheflabs/reanimated-flashlist",
3
+ "version": "0.1.7",
4
+ "description": "A high-performance animated FlashList with drag-to-reorder and entry/exit animations (New Architecture)",
5
+ "main": "lib/index.js",
6
+ "module": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "source": "src/index.ts",
9
+ "files": [
10
+ "lib",
11
+ "src"
12
+ ],
13
+ "sideEffects": false,
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit",
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch",
19
+ "test:coverage": "jest --coverage",
20
+ "tag": "./scripts/tag.sh",
21
+ "release:patch": "./scripts/release.sh patch",
22
+ "release:minor": "./scripts/release.sh minor",
23
+ "release:major": "./scripts/release.sh major",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "peerDependencies": {
27
+ "@shopify/flash-list": "^2.0.0",
28
+ "react": ">=18.0.0",
29
+ "react-native": ">=0.74.0",
30
+ "react-native-gesture-handler": ">=2.14.0",
31
+ "react-native-reanimated": "^4.0.0",
32
+ "react-native-worklets": ">=0.7.0"
33
+ },
34
+ "devDependencies": {
35
+ "@babel/core": "^7.24.0",
36
+ "@babel/preset-env": "^7.24.0",
37
+ "@babel/preset-react": "^7.24.0",
38
+ "@babel/preset-typescript": "^7.24.0",
39
+ "@shopify/flash-list": "^2.0.0",
40
+ "@testing-library/react-native": "^12.4.0",
41
+ "@types/jest": "^29.5.0",
42
+ "@types/react": "^18.2.0",
43
+ "@types/react-native": "^0.72.0",
44
+ "babel-jest": "^29.7.0",
45
+ "jest": "^29.7.0",
46
+ "react": "^18.2.0",
47
+ "react-native": "^0.76.0",
48
+ "react-native-gesture-handler": "^2.14.0",
49
+ "react-native-reanimated": "^4.0.0",
50
+ "react-test-renderer": "^18.2.0",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "keywords": [
54
+ "react-native",
55
+ "flashlist",
56
+ "animated",
57
+ "drag-to-reorder",
58
+ "reanimated",
59
+ "layout-animation",
60
+ "new-architecture",
61
+ "turbomodules"
62
+ ],
63
+ "author": "SousChef Labs",
64
+ "license": "MIT",
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "https://github.com/SousChefLabs/reanimated-flashlist.git"
68
+ },
69
+ "bugs": {
70
+ "url": "https://github.com/SousChefLabs/reanimated-flashlist/issues"
71
+ },
72
+ "homepage": "https://github.com/SousChefLabs/reanimated-flashlist#readme"
73
+ }
@@ -0,0 +1,411 @@
1
+ import React, {
2
+ useCallback,
3
+ useRef,
4
+ useMemo,
5
+ forwardRef,
6
+ useImperativeHandle,
7
+ useEffect,
8
+ } from 'react';
9
+ import {
10
+ View,
11
+ RefreshControl,
12
+ type NativeSyntheticEvent,
13
+ type NativeScrollEvent,
14
+ type LayoutChangeEvent,
15
+ type ViewStyle,
16
+ } from 'react-native';
17
+ import { FlashList, type ListRenderItemInfo, type FlashListRef } from '@shopify/flash-list';
18
+ import { DragStateProvider, ListAnimationProvider, useDragState } from './contexts';
19
+ import { AnimatedFlashListItem } from './AnimatedFlashListItem';
20
+ import { DEFAULT_DRAG_CONFIG } from './constants';
21
+ import type {
22
+ AnimatedListItem,
23
+ AnimatedFlashListProps,
24
+ AnimatedFlashListRef,
25
+ HapticFeedbackType,
26
+ } from './types';
27
+
28
+ /**
29
+ * Item wrapper component for FlashList
30
+ * Uses ref for totalItems to avoid renderItem callback recreation
31
+ */
32
+ interface ItemWrapperProps<T extends AnimatedListItem> {
33
+ item: T;
34
+ index: number;
35
+ totalItemsRef: React.RefObject<number>;
36
+ renderItem: AnimatedFlashListProps<T>['renderItem'];
37
+ canDrag?: (item: T, index: number) => boolean;
38
+ dragEnabled: boolean;
39
+ onReorderByDelta?: (itemId: string, delta: number) => void;
40
+ onHapticFeedback?: (type: HapticFeedbackType) => void;
41
+ }
42
+
43
+ const ItemWrapper = React.memo(function ItemWrapper<T extends AnimatedListItem>({
44
+ item,
45
+ index,
46
+ totalItemsRef,
47
+ renderItem,
48
+ canDrag,
49
+ dragEnabled,
50
+ onReorderByDelta,
51
+ onHapticFeedback,
52
+ }: ItemWrapperProps<T>) {
53
+ const isDragEnabled = dragEnabled && (canDrag ? canDrag(item, index) : true);
54
+
55
+ return (
56
+ <AnimatedFlashListItem
57
+ item={item}
58
+ index={index}
59
+ totalItems={totalItemsRef.current ?? 0}
60
+ isDragEnabled={isDragEnabled}
61
+ renderItem={renderItem}
62
+ onReorderByDelta={onReorderByDelta}
63
+ onHapticFeedback={onHapticFeedback}
64
+ />
65
+ );
66
+ }) as <T extends AnimatedListItem>(props: ItemWrapperProps<T>) => React.ReactElement;
67
+
68
+ /**
69
+ * Inner FlashList component that uses DragStateContext for scroll tracking
70
+ */
71
+ interface InnerFlashListProps<T extends AnimatedListItem> {
72
+ data: T[];
73
+ totalItemsRef: React.RefObject<number>;
74
+ flashListRef: React.RefObject<FlashListRef<T> | null>;
75
+ renderItem: AnimatedFlashListProps<T>['renderItem'];
76
+ keyExtractor: (item: T, index: number) => string;
77
+ canDrag?: (item: T, index: number) => boolean;
78
+ dragEnabled: boolean;
79
+ onReorderByDelta?: (itemId: string, delta: number) => void;
80
+ onHapticFeedback?: (type: HapticFeedbackType) => void;
81
+ itemHeight: number;
82
+ ListFooterComponent?: AnimatedFlashListProps<T>['ListFooterComponent'];
83
+ onEndReached?: () => void;
84
+ onEndReachedThreshold?: number;
85
+ onRefresh?: () => void | Promise<void>;
86
+ refreshing?: boolean;
87
+ refreshTintColor?: string;
88
+ contentContainerStyle?: AnimatedFlashListProps<T>['contentContainerStyle'];
89
+ drawDistance?: number;
90
+ showsVerticalScrollIndicator?: boolean;
91
+ }
92
+
93
+ function InnerFlashList<T extends AnimatedListItem>({
94
+ data,
95
+ totalItemsRef,
96
+ flashListRef,
97
+ renderItem,
98
+ keyExtractor,
99
+ canDrag,
100
+ dragEnabled,
101
+ onReorderByDelta,
102
+ onHapticFeedback,
103
+ itemHeight,
104
+ ListFooterComponent,
105
+ onEndReached,
106
+ onEndReachedThreshold,
107
+ onRefresh,
108
+ refreshing,
109
+ refreshTintColor,
110
+ contentContainerStyle,
111
+ drawDistance = 500,
112
+ showsVerticalScrollIndicator = true,
113
+ }: InnerFlashListProps<T>): React.ReactElement {
114
+ // Get drag state context for scroll tracking
115
+ const { scrollOffset, contentHeight, visibleHeight, listTopY, setListRef } =
116
+ useDragState();
117
+
118
+ // Register FlashList ref with drag context for autoscroll
119
+ useEffect(() => {
120
+ // Cast to unknown to satisfy the generic constraint
121
+ setListRef(flashListRef.current as FlashListRef<unknown> | null);
122
+ return () => setListRef(null);
123
+ }, [setListRef, flashListRef]);
124
+
125
+ // Update scroll offset on scroll
126
+ const handleScroll = useCallback(
127
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
128
+ scrollOffset.value = event.nativeEvent.contentOffset.y;
129
+ },
130
+ [scrollOffset],
131
+ );
132
+
133
+ // Update content height when list content changes
134
+ const handleContentSizeChange = useCallback(
135
+ (_width: number, height: number) => {
136
+ contentHeight.value = height;
137
+ },
138
+ [contentHeight],
139
+ );
140
+
141
+ // Update visible height and list position when layout changes
142
+ const handleLayout = useCallback(
143
+ (event: LayoutChangeEvent) => {
144
+ visibleHeight.value = event.nativeEvent.layout.height;
145
+ const nativeRef = flashListRef.current?.getNativeScrollRef?.() as
146
+ | { measureInWindow?: (cb: (x: number, y: number) => void) => void }
147
+ | undefined;
148
+ if (nativeRef?.measureInWindow) {
149
+ nativeRef.measureInWindow((_x, y) => {
150
+ listTopY.value = y;
151
+ });
152
+ }
153
+ },
154
+ [visibleHeight, listTopY, flashListRef],
155
+ );
156
+
157
+ // Render item for FlashList
158
+ const flashListRenderItem = useCallback(
159
+ ({ item, index }: ListRenderItemInfo<T>) => (
160
+ <ItemWrapper
161
+ item={item}
162
+ index={index}
163
+ totalItemsRef={totalItemsRef}
164
+ renderItem={renderItem}
165
+ canDrag={canDrag}
166
+ dragEnabled={dragEnabled}
167
+ onReorderByDelta={onReorderByDelta}
168
+ onHapticFeedback={onHapticFeedback}
169
+ />
170
+ ),
171
+ [
172
+ totalItemsRef,
173
+ renderItem,
174
+ canDrag,
175
+ dragEnabled,
176
+ onReorderByDelta,
177
+ onHapticFeedback,
178
+ ],
179
+ );
180
+
181
+ // getItemType for FlashList recycling optimization
182
+ const getItemType = useCallback(() => 'animated-item', []);
183
+
184
+ // Override item layout for consistent drag calculations
185
+ // Note: We cast the layout to include size for drag calculations
186
+ const overrideItemLayout = useCallback(
187
+ (layout: { span?: number }, _item: T, _index: number) => {
188
+ // FlashList v2 uses this for span, but we extend for size in drag calculations
189
+ (layout as { size?: number }).size = itemHeight;
190
+ },
191
+ [itemHeight],
192
+ );
193
+
194
+ return (
195
+ <FlashList
196
+ ref={flashListRef as React.RefObject<FlashListRef<T>>}
197
+ data={data}
198
+ renderItem={flashListRenderItem}
199
+ keyExtractor={keyExtractor}
200
+ getItemType={getItemType}
201
+ overrideItemLayout={overrideItemLayout}
202
+ onScroll={handleScroll}
203
+ onContentSizeChange={handleContentSizeChange}
204
+ onLayout={handleLayout}
205
+ scrollEventThrottle={16}
206
+ drawDistance={drawDistance}
207
+ maintainVisibleContentPosition={{ disabled: true }}
208
+ showsVerticalScrollIndicator={showsVerticalScrollIndicator}
209
+ contentContainerStyle={contentContainerStyle}
210
+ ListFooterComponent={ListFooterComponent ?? undefined}
211
+ onEndReached={onEndReached}
212
+ onEndReachedThreshold={onEndReachedThreshold}
213
+ refreshControl={
214
+ onRefresh ? (
215
+ <RefreshControl
216
+ refreshing={refreshing ?? false}
217
+ onRefresh={onRefresh}
218
+ tintColor={refreshTintColor}
219
+ colors={refreshTintColor ? [refreshTintColor] : undefined}
220
+ />
221
+ ) : undefined
222
+ }
223
+ />
224
+ );
225
+ }
226
+
227
+ /**
228
+ * AnimatedFlashList - High-performance animated list with drag-to-reorder
229
+ *
230
+ * A wrapper around @shopify/flash-list that provides:
231
+ * - Smooth drag-to-reorder with autoscroll
232
+ * - Entry animations for new items
233
+ * - Exit animations for removed items
234
+ * - Full TypeScript generics support
235
+ *
236
+ * @example
237
+ * ```tsx
238
+ * <AnimatedFlashList<MyItem>
239
+ * data={items}
240
+ * keyExtractor={(item) => item.id}
241
+ * renderItem={({ item, animatedStyle, dragHandleProps }) => (
242
+ * <Animated.View style={animatedStyle}>
243
+ * <MyItem item={item} />
244
+ * {dragHandleProps && (
245
+ * <GestureDetector gesture={dragHandleProps.gesture}>
246
+ * <DragHandle />
247
+ * </GestureDetector>
248
+ * )}
249
+ * </Animated.View>
250
+ * )}
251
+ * dragEnabled
252
+ * onReorder={(itemId, from, to) => reorderItems(itemId, from, to)}
253
+ * />
254
+ * ```
255
+ */
256
+ function AnimatedFlashListInner<T extends AnimatedListItem>(
257
+ props: AnimatedFlashListProps<T>,
258
+ ref: React.ForwardedRef<AnimatedFlashListRef>,
259
+ ): React.ReactElement | null {
260
+ const {
261
+ data,
262
+ keyExtractor,
263
+ renderItem,
264
+ dragEnabled = false,
265
+ onReorder,
266
+ onReorderByNeighbors,
267
+ canDrag,
268
+ onHapticFeedback,
269
+ config,
270
+ onPrepareLayoutAnimation,
271
+ ListFooterComponent,
272
+ onRefresh,
273
+ refreshing = false,
274
+ onEndReached,
275
+ onEndReachedThreshold = 0.5,
276
+ contentContainerStyle,
277
+ ...flashListProps
278
+ } = props;
279
+
280
+ // Merge config with defaults
281
+ const dragConfig = useMemo(
282
+ () => ({
283
+ ...DEFAULT_DRAG_CONFIG,
284
+ ...config?.drag,
285
+ }),
286
+ [config?.drag],
287
+ );
288
+
289
+ // Ref to FlashList
290
+ const flashListRef = useRef<FlashListRef<T>>(null);
291
+
292
+ // Expose methods to parent via ref
293
+ useImperativeHandle(ref, () => ({
294
+ prepareForLayoutAnimation: () => {
295
+ flashListRef.current?.prepareForLayoutAnimationRender();
296
+ onPrepareLayoutAnimation?.();
297
+ },
298
+ scrollToOffset: (offset: number, animated = true) => {
299
+ flashListRef.current?.scrollToOffset({ offset, animated });
300
+ },
301
+ scrollToIndex: (index: number, animated = true) => {
302
+ flashListRef.current?.scrollToIndex({ index, animated });
303
+ },
304
+ }));
305
+
306
+ // Keep valid items in ref for reorder callback
307
+ const dataRef = useRef<T[]>([]);
308
+ dataRef.current = data;
309
+
310
+ // Handle reorder by index delta - converts to various callback formats
311
+ const handleReorderByDelta = useCallback(
312
+ (itemId: string, indexDelta: number) => {
313
+ if (indexDelta === 0) return;
314
+
315
+ const currentItems = dataRef.current;
316
+ const currentIndex = currentItems.findIndex(item => item.id === itemId);
317
+ if (currentIndex === -1) return;
318
+
319
+ const newIndex = Math.max(
320
+ 0,
321
+ Math.min(currentItems.length - 1, currentIndex + indexDelta),
322
+ );
323
+ if (newIndex === currentIndex) return;
324
+
325
+ // Call onReorder if provided
326
+ if (onReorder) {
327
+ onReorder(itemId, currentIndex, newIndex);
328
+ }
329
+
330
+ // Call onReorderByNeighbors if provided (for fractional indexing)
331
+ if (onReorderByNeighbors) {
332
+ let afterItemId: string | null = null;
333
+ let beforeItemId: string | null = null;
334
+
335
+ if (indexDelta > 0) {
336
+ afterItemId = currentItems[newIndex]?.id ?? null;
337
+ beforeItemId =
338
+ newIndex < currentItems.length - 1
339
+ ? currentItems[newIndex + 1]?.id ?? null
340
+ : null;
341
+ } else {
342
+ afterItemId =
343
+ newIndex > 0 ? currentItems[newIndex - 1]?.id ?? null : null;
344
+ beforeItemId = currentItems[newIndex]?.id ?? null;
345
+ }
346
+
347
+ onReorderByNeighbors(itemId, afterItemId, beforeItemId);
348
+ }
349
+ },
350
+ [onReorder, onReorderByNeighbors],
351
+ );
352
+
353
+ // Use ref for totalItems to avoid renderItem callback recreation
354
+ const totalItemsRef = useRef(data.length);
355
+ totalItemsRef.current = data.length;
356
+
357
+ // Early return for empty data
358
+ if (!data || !Array.isArray(data) || data.length === 0) {
359
+ if (ListFooterComponent) {
360
+ return (
361
+ <View style={containerStyle}>
362
+ {React.isValidElement(ListFooterComponent)
363
+ ? ListFooterComponent
364
+ : React.createElement(ListFooterComponent as React.ComponentType)}
365
+ </View>
366
+ );
367
+ }
368
+ return null;
369
+ }
370
+
371
+ return (
372
+ <ListAnimationProvider>
373
+ <DragStateProvider config={dragConfig}>
374
+ <View style={containerStyle}>
375
+ <InnerFlashList
376
+ data={data}
377
+ totalItemsRef={totalItemsRef}
378
+ flashListRef={flashListRef}
379
+ renderItem={renderItem}
380
+ keyExtractor={keyExtractor}
381
+ canDrag={canDrag}
382
+ dragEnabled={dragEnabled}
383
+ onReorderByDelta={
384
+ onReorder || onReorderByNeighbors ? handleReorderByDelta : undefined
385
+ }
386
+ onHapticFeedback={onHapticFeedback}
387
+ itemHeight={dragConfig.itemHeight}
388
+ ListFooterComponent={ListFooterComponent}
389
+ onEndReached={onEndReached}
390
+ onEndReachedThreshold={onEndReachedThreshold}
391
+ onRefresh={onRefresh}
392
+ refreshing={refreshing}
393
+ contentContainerStyle={contentContainerStyle}
394
+ {...flashListProps}
395
+ />
396
+ </View>
397
+ </DragStateProvider>
398
+ </ListAnimationProvider>
399
+ );
400
+ }
401
+
402
+ const containerStyle: ViewStyle = {
403
+ flex: 1,
404
+ };
405
+
406
+ // Forward ref with generic support
407
+ export const AnimatedFlashList = forwardRef(AnimatedFlashListInner) as <
408
+ T extends AnimatedListItem,
409
+ >(
410
+ props: AnimatedFlashListProps<T> & { ref?: React.ForwardedRef<AnimatedFlashListRef> },
411
+ ) => React.ReactElement | null;