react-native-swappable-grid 1.0.0

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 (44) hide show
  1. package/README.md +284 -0
  2. package/lib/commonjs/ChildWrapper.js +320 -0
  3. package/lib/commonjs/ChildWrapper.js.map +1 -0
  4. package/lib/commonjs/SwappableGrid.js +378 -0
  5. package/lib/commonjs/SwappableGrid.js.map +1 -0
  6. package/lib/commonjs/index.js +14 -0
  7. package/lib/commonjs/index.js.map +1 -0
  8. package/lib/commonjs/utils/helpers/computerMinHeight.js +11 -0
  9. package/lib/commonjs/utils/helpers/computerMinHeight.js.map +1 -0
  10. package/lib/commonjs/utils/helpers/gestures/PanWithLongPress.js +249 -0
  11. package/lib/commonjs/utils/helpers/gestures/PanWithLongPress.js.map +1 -0
  12. package/lib/commonjs/utils/helpers/indexCalculations.js +73 -0
  13. package/lib/commonjs/utils/helpers/indexCalculations.js.map +1 -0
  14. package/lib/commonjs/utils/useGridLayout.js +205 -0
  15. package/lib/commonjs/utils/useGridLayout.js.map +1 -0
  16. package/lib/module/ChildWrapper.js +313 -0
  17. package/lib/module/ChildWrapper.js.map +1 -0
  18. package/lib/module/SwappableGrid.js +370 -0
  19. package/lib/module/SwappableGrid.js.map +1 -0
  20. package/lib/module/index.js +2 -0
  21. package/lib/module/index.js.map +1 -0
  22. package/lib/module/utils/helpers/computerMinHeight.js +5 -0
  23. package/lib/module/utils/helpers/computerMinHeight.js.map +1 -0
  24. package/lib/module/utils/helpers/gestures/PanWithLongPress.js +242 -0
  25. package/lib/module/utils/helpers/gestures/PanWithLongPress.js.map +1 -0
  26. package/lib/module/utils/helpers/indexCalculations.js +64 -0
  27. package/lib/module/utils/helpers/indexCalculations.js.map +1 -0
  28. package/lib/module/utils/useGridLayout.js +199 -0
  29. package/lib/module/utils/useGridLayout.js.map +1 -0
  30. package/lib/typescript/ChildWrapper.d.ts +23 -0
  31. package/lib/typescript/SwappableGrid.d.ts +85 -0
  32. package/lib/typescript/index.d.ts +2 -0
  33. package/lib/typescript/utils/helpers/computerMinHeight.d.ts +1 -0
  34. package/lib/typescript/utils/helpers/gestures/PanWithLongPress.d.ts +40 -0
  35. package/lib/typescript/utils/helpers/indexCalculations.d.ts +28 -0
  36. package/lib/typescript/utils/useGridLayout.d.ts +46 -0
  37. package/package.json +68 -0
  38. package/src/ChildWrapper.tsx +376 -0
  39. package/src/SwappableGrid.tsx +492 -0
  40. package/src/index.ts +2 -0
  41. package/src/utils/helpers/computerMinHeight.ts +9 -0
  42. package/src/utils/helpers/gestures/PanWithLongPress.ts +304 -0
  43. package/src/utils/helpers/indexCalculations.ts +91 -0
  44. package/src/utils/useGridLayout.ts +236 -0
@@ -0,0 +1,304 @@
1
+ import { Gesture } from "react-native-gesture-handler";
2
+ import {
3
+ runOnJS,
4
+ SharedValue,
5
+ withSpring,
6
+ withTiming,
7
+ scrollTo,
8
+ AnimatedRef,
9
+ useSharedValue,
10
+ useDerivedValue,
11
+ } from "react-native-reanimated";
12
+ import {
13
+ indexToXY,
14
+ toIndex1ColFromLiveMidlines,
15
+ xyToIndex,
16
+ } from "../indexCalculations";
17
+
18
+ interface PanProps {
19
+ order: SharedValue<string[]>;
20
+ dynamicNumColumns: SharedValue<number>;
21
+ activeKey: SharedValue<string | null>;
22
+ offsetX: SharedValue<number>;
23
+ offsetY: SharedValue<number>;
24
+ startX: SharedValue<number>;
25
+ startY: SharedValue<number>;
26
+ dragMode: SharedValue<boolean>;
27
+ positions: any;
28
+ itemsByKey: any;
29
+ itemWidth: number;
30
+ itemHeight: number;
31
+ containerPadding: number;
32
+ gap: number;
33
+ setOrderState: React.Dispatch<React.SetStateAction<string[]>>;
34
+ onDragEnd?: (ordered: ChildNode[]) => void;
35
+ onOrderChange?: (keys: string[]) => void;
36
+
37
+ // scrolling
38
+ scrollSpeed: number;
39
+ scrollThreshold: number;
40
+ scrollViewRef: AnimatedRef<any>;
41
+ scrollOffset: SharedValue<number>;
42
+ viewportH: SharedValue<number>;
43
+ longPressMs: number;
44
+ contentH: SharedValue<number>;
45
+ reverse?: boolean;
46
+ deleteComponentPosition?: SharedValue<{
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ } | null>;
52
+ deleteItem?: (key: string) => void;
53
+ contentPaddingBottom?: number; // Padding bottom from style prop to allow dragging into padding area
54
+ }
55
+
56
+ export const PanWithLongPress = (props: PanProps & { longPressMs: number }) => {
57
+ const {
58
+ order,
59
+ dynamicNumColumns,
60
+ activeKey,
61
+ offsetX,
62
+ offsetY,
63
+ startX,
64
+ startY,
65
+ dragMode,
66
+ positions,
67
+ itemsByKey,
68
+ itemWidth,
69
+ itemHeight,
70
+ containerPadding,
71
+ gap,
72
+ setOrderState,
73
+ onDragEnd,
74
+ onOrderChange,
75
+ scrollSpeed,
76
+ scrollThreshold,
77
+ scrollViewRef,
78
+ scrollOffset,
79
+ viewportH,
80
+ longPressMs,
81
+ contentH,
82
+ reverse = false,
83
+ deleteComponentPosition,
84
+ deleteItem,
85
+ contentPaddingBottom = 0,
86
+ } = props;
87
+
88
+ const scrollDir = useSharedValue(0); // -1 = up, 1 = down, 0 = none
89
+ const initialScrollOffset = useSharedValue(0);
90
+
91
+ useDerivedValue(() => {
92
+ if (!dragMode.value || !activeKey.value) return;
93
+
94
+ if (viewportH.value <= 0 || contentH.value <= 0) return;
95
+
96
+ const key = activeKey.value;
97
+ const p = positions[key];
98
+ if (!p) return;
99
+
100
+ // 1. Clamp scroll offset
101
+ const maxScroll = contentH.value - viewportH.value;
102
+ const newScroll = Math.max(
103
+ 0,
104
+ Math.min(scrollOffset.value + scrollDir.value * scrollSpeed, maxScroll)
105
+ );
106
+
107
+ scrollTo(scrollViewRef, 0, newScroll, false);
108
+ const scrollDelta = newScroll - initialScrollOffset.value;
109
+ scrollOffset.value = newScroll;
110
+
111
+ // 2. Clamp item position
112
+ // Allow dragging into padding area (paddingBottom from style prop)
113
+ const minY = 0;
114
+ // Add paddingBottom to maxY to allow dragging into the padding area
115
+ const maxY = contentH.value - itemHeight + contentPaddingBottom;
116
+ const proposedY = startY.value + offsetY.value + scrollDelta;
117
+ p.y.value = Math.max(minY, Math.min(proposedY, maxY));
118
+
119
+ // X stays normal
120
+ p.x.value = startX.value + offsetX.value;
121
+
122
+ // Keep loop alive
123
+ requestAnimationFrame(() => {
124
+ scrollDir.value = scrollDir.value;
125
+ });
126
+ });
127
+
128
+ const getIndexOfKey = (key: string) => {
129
+ "worklet";
130
+ return order.value.findIndex((x) => x === key);
131
+ };
132
+
133
+ return Gesture.Pan()
134
+ .minDistance(10)
135
+ .activateAfterLongPress(longPressMs)
136
+ .onStart(({ x, y }) => {
137
+ initialScrollOffset.value = scrollOffset.value;
138
+ dragMode.value = true;
139
+ let bestKey: string | null = null;
140
+ let bestDist = Number.MAX_VALUE;
141
+ order.value.forEach((key) => {
142
+ const p = positions[key];
143
+ if (!p) return;
144
+ const cx = p.x.value + itemWidth / 2;
145
+ const cy = p.y.value + itemHeight / 2;
146
+ const dx = cx - x;
147
+ const dy = cy - y;
148
+ const dist2 = dx * dx + dy * dy;
149
+ if (dist2 < bestDist) {
150
+ bestDist = dist2;
151
+ bestKey = key;
152
+ }
153
+ });
154
+ if (!bestKey) return;
155
+ activeKey.value = bestKey;
156
+ const p = positions[bestKey]!;
157
+ p.active.value = withTiming(1, { duration: 120 });
158
+ startX.value = p.x.value;
159
+ startY.value = p.y.value;
160
+ offsetX.value = 0;
161
+ offsetY.value = 0;
162
+ })
163
+ .onUpdate(({ translationX, translationY }) => {
164
+ if (!dragMode.value) return;
165
+ const key = activeKey.value;
166
+ if (!key) return;
167
+
168
+ const p = positions[key]!;
169
+ const scrollDelta = scrollOffset.value - initialScrollOffset.value;
170
+
171
+ // Update active (top-left)
172
+ offsetX.value = translationX;
173
+ offsetY.value = translationY;
174
+ p.x.value = startX.value + offsetX.value;
175
+ p.y.value = startY.value + offsetY.value + scrollDelta;
176
+
177
+ // Auto-scroll (unchanged)
178
+ const pointerYInViewport = p.y.value - scrollOffset.value;
179
+ if (pointerYInViewport > viewportH.value - scrollThreshold) {
180
+ scrollDir.value = 1;
181
+ } else if (pointerYInViewport < scrollThreshold) {
182
+ scrollDir.value = -1;
183
+ } else {
184
+ scrollDir.value = 0;
185
+ }
186
+
187
+ // Compute target index from the active tile's **center**
188
+ const centerY = p.y.value + itemHeight / 2;
189
+ const fromIndex = getIndexOfKey(key);
190
+
191
+ let toIndex: number;
192
+ if (dynamicNumColumns.value === 1) {
193
+ toIndex = toIndex1ColFromLiveMidlines(
194
+ order,
195
+ positions,
196
+ activeKey,
197
+ itemHeight,
198
+ centerY,
199
+ reverse // ← pass your prop
200
+ );
201
+ } else {
202
+ // unchanged multi-column path
203
+ const centerX = p.x.value + itemWidth / 2;
204
+ toIndex = xyToIndex({
205
+ order,
206
+ x: centerX,
207
+ y: centerY,
208
+ itemWidth,
209
+ itemHeight,
210
+ dynamicNumColumns,
211
+ containerPadding,
212
+ gap,
213
+ });
214
+ }
215
+
216
+ if (
217
+ toIndex !== fromIndex &&
218
+ toIndex >= 0 &&
219
+ toIndex <= order.value.length - 1
220
+ ) {
221
+ const next = [...order.value];
222
+ next.splice(fromIndex, 1);
223
+ next.splice(toIndex, 0, key);
224
+ order.value = next;
225
+ }
226
+ })
227
+ .onEnd(() => {
228
+ scrollDir.value = 0; // stop auto-scroll
229
+ if (!dragMode.value) return;
230
+ const key = activeKey.value;
231
+ if (!key) {
232
+ dragMode.value = false;
233
+ return;
234
+ }
235
+ const p = positions[key]!;
236
+
237
+ // Check if item was dropped into delete component
238
+ if (deleteComponentPosition?.value && deleteItem) {
239
+ const deletePos = deleteComponentPosition.value;
240
+
241
+ // Add tolerance/padding to make it easier to hit (20% of item size)
242
+ const tolerance = Math.min(itemWidth, itemHeight) * 0.2;
243
+ const expandedDeleteX = deletePos.x - tolerance;
244
+ const expandedDeleteY = deletePos.y - tolerance;
245
+ const expandedDeleteWidth = deletePos.width + tolerance * 2;
246
+ const expandedDeleteHeight = deletePos.height + tolerance * 2;
247
+
248
+ // Check if item bounding box overlaps with expanded delete component bounds
249
+ // This is more forgiving than checking just the center point
250
+ const itemLeft = p.x.value;
251
+ const itemRight = p.x.value + itemWidth;
252
+ const itemTop = p.y.value;
253
+ const itemBottom = p.y.value + itemHeight;
254
+
255
+ // Bounding box intersection check
256
+ const overlaps =
257
+ itemLeft < expandedDeleteX + expandedDeleteWidth &&
258
+ itemRight > expandedDeleteX &&
259
+ itemTop < expandedDeleteY + expandedDeleteHeight &&
260
+ itemBottom > expandedDeleteY;
261
+
262
+ if (overlaps) {
263
+ // Item was dropped into delete component - delete it
264
+ runOnJS(deleteItem)(key);
265
+ // Note: deleteItem will handle calling onDelete callback if provided
266
+ p.active.value = withTiming(0, { duration: 120 });
267
+ activeKey.value = null;
268
+ dragMode.value = false;
269
+ return;
270
+ }
271
+ }
272
+
273
+ // Normal drop - return to grid position
274
+ const idx = getIndexOfKey(key);
275
+ const { x, y } = indexToXY({
276
+ index: idx,
277
+ itemWidth,
278
+ itemHeight,
279
+ dynamicNumColumns,
280
+ containerPadding,
281
+ gap,
282
+ });
283
+ const scale = Math.min(itemWidth, itemHeight) / 200; // 100px baseline
284
+
285
+ const damping = 18 * scale;
286
+ const stiffness = 240 * scale;
287
+ const mass = Math.max(0.05, scale); // helps stability for tiny items
288
+
289
+ p.x.value = withSpring(x, { damping, stiffness, mass });
290
+ p.y.value = withSpring(y, { damping, stiffness, mass });
291
+
292
+ p.active.value = withTiming(0, { duration: 120 });
293
+
294
+ runOnJS(setOrderState)(order.value);
295
+ if (onDragEnd) {
296
+ runOnJS(onDragEnd)(order.value.map((key) => itemsByKey[key]));
297
+ }
298
+ if (onOrderChange) {
299
+ runOnJS(onOrderChange)([...order.value]);
300
+ }
301
+ activeKey.value = null;
302
+ dragMode.value = false;
303
+ });
304
+ };
@@ -0,0 +1,91 @@
1
+ import { SharedValue } from "react-native-reanimated";
2
+
3
+ interface IndexToXYProps {
4
+ index: number;
5
+ itemHeight: number;
6
+ itemWidth: number;
7
+ dynamicNumColumns: SharedValue<number>;
8
+ containerPadding: number;
9
+ gap: number;
10
+ }
11
+
12
+ export const indexToXY = ({
13
+ index,
14
+ itemHeight,
15
+ itemWidth,
16
+ dynamicNumColumns,
17
+ containerPadding,
18
+ gap,
19
+ }: IndexToXYProps) => {
20
+ "worklet";
21
+ const cols = dynamicNumColumns.value;
22
+ const col = index % cols;
23
+ const row = Math.floor(index / cols);
24
+ const x = containerPadding + col * (itemWidth + gap);
25
+ const y = containerPadding + row * (itemHeight + gap);
26
+ return { x, y };
27
+ };
28
+
29
+ interface XYToIndexProps {
30
+ order: SharedValue<string[]>;
31
+ x: number;
32
+ y: number;
33
+ itemHeight: number;
34
+ itemWidth: number;
35
+ dynamicNumColumns: SharedValue<number>;
36
+ containerPadding: number;
37
+ gap: number;
38
+ }
39
+
40
+ export const xyToIndex = ({
41
+ order,
42
+ x,
43
+ y,
44
+ itemHeight,
45
+ itemWidth,
46
+ dynamicNumColumns,
47
+ gap,
48
+ containerPadding,
49
+ }: XYToIndexProps) => {
50
+ "worklet";
51
+ const cols = dynamicNumColumns.value;
52
+
53
+ // Work with CENTER coordinates relative to the content box
54
+ const relX = x - containerPadding;
55
+ const relY = y - containerPadding;
56
+
57
+ const col = Math.floor(relX / (itemWidth + gap));
58
+ const row = Math.floor(relY / (itemHeight + gap));
59
+
60
+ const clampedCol = Math.max(0, Math.min(cols - 1, col));
61
+ const maxRows = Math.max(1, Math.ceil(order.value.length / cols));
62
+ const clampedRow = Math.max(0, Math.min(maxRows - 1, row));
63
+
64
+ return clampedRow * cols + clampedCol;
65
+ };
66
+
67
+ export const toIndex1ColFromLiveMidlines = (
68
+ order: SharedValue<string[]>,
69
+ positions: Record<string, { y: SharedValue<number> }>,
70
+ activeKey: SharedValue<string | null>,
71
+ itemHeight: number,
72
+ centerY: number,
73
+ reverse: boolean
74
+ ) => {
75
+ "worklet";
76
+ const list = order.value.filter((k) => k !== activeKey.value);
77
+
78
+ // Sort by actual on-screen Y (top → bottom)
79
+ list.sort((a, b) => positions[a].y.value - positions[b].y.value);
80
+
81
+ // Find visual slot v (0..list.length)
82
+ let v = 0;
83
+ for (; v < list.length; v++) {
84
+ const mid = positions[list[v]].y.value + itemHeight / 2;
85
+ if (centerY < mid) break;
86
+ }
87
+
88
+ // Map visual slot → array index AFTER removal (reverse aware)
89
+ const afterRemovalLen = list.length;
90
+ return reverse ? afterRemovalLen - v : v;
91
+ };
@@ -0,0 +1,236 @@
1
+ import { Children, isValidElement, ReactNode, useMemo, useState } from "react";
2
+ import { LayoutChangeEvent } from "react-native";
3
+ import { Gesture } from "react-native-gesture-handler";
4
+ import {
5
+ AnimatedRef,
6
+ SharedValue,
7
+ useAnimatedScrollHandler,
8
+ useDerivedValue,
9
+ useSharedValue,
10
+ withSpring,
11
+ } from "react-native-reanimated";
12
+ import { indexToXY } from "./helpers/indexCalculations";
13
+ import { PanWithLongPress } from "./helpers/gestures/PanWithLongPress";
14
+
15
+ interface useGridLayoutProps {
16
+ reverse?: boolean;
17
+ children: ReactNode;
18
+ itemWidth: number;
19
+ itemHeight: number;
20
+ gap: number;
21
+ containerPadding: number;
22
+ longPressMs: number;
23
+ numColumns?: number;
24
+ onDragEnd?: (ordered: ChildNode[]) => void;
25
+ onOrderChange?: (keys: string[]) => void;
26
+ onDelete?: (key: string) => void;
27
+ scrollViewRef: AnimatedRef<any>;
28
+ scrollSpeed: number;
29
+ scrollThreshold: number;
30
+ contentPaddingBottom?: number;
31
+ }
32
+
33
+ export function useGridLayout({
34
+ reverse,
35
+ children,
36
+ itemWidth,
37
+ itemHeight,
38
+ gap,
39
+ containerPadding,
40
+ longPressMs,
41
+ numColumns,
42
+ onDragEnd,
43
+ onOrderChange,
44
+ onDelete,
45
+ scrollViewRef,
46
+ scrollSpeed,
47
+ scrollThreshold,
48
+ contentPaddingBottom = 0,
49
+ }: useGridLayoutProps) {
50
+ const childArray = Children.toArray(children).filter(isValidElement);
51
+ const keys = childArray.map((child) => {
52
+ if (!("key" in child) || child.key == null) {
53
+ throw new Error("All children must have a unique 'key' prop.");
54
+ }
55
+ return String(child.key);
56
+ });
57
+
58
+ const [orderState, setOrderState] = useState(keys);
59
+
60
+ const itemsByKey = useMemo(() => {
61
+ const map: Record<string, ReactNode> = {};
62
+ childArray.forEach((child) => {
63
+ map[String(child.key)] = child;
64
+ });
65
+ return map;
66
+ }, [children]);
67
+
68
+ const dynamicNumColumns: SharedValue<number> = useSharedValue(
69
+ numColumns ? numColumns : 1
70
+ );
71
+ const order = useSharedValue<string[]>(orderState);
72
+ const contentW = useSharedValue(0);
73
+ const viewportH = useSharedValue(0); // **visible height of ScrollView**
74
+ const activeKey = useSharedValue<string | null>(null);
75
+ const offsetX = useSharedValue(0);
76
+ const offsetY = useSharedValue(0);
77
+ const startX = useSharedValue(0);
78
+ const startY = useSharedValue(0);
79
+ const dragMode = useSharedValue(false);
80
+ const anyItemInDeleteMode = useSharedValue(false); // Global delete mode state
81
+ const contentH = useSharedValue(0);
82
+ const scrollOffset = useSharedValue(0);
83
+
84
+ const onScroll = useAnimatedScrollHandler({
85
+ onScroll: (e) => {
86
+ scrollOffset.value = e.contentOffset.y;
87
+ },
88
+ });
89
+
90
+ // initial positions (create shared values consistently by data length)
91
+ const positionsArray = childArray.map((d, i) => {
92
+ const { x, y } = indexToXY({
93
+ index: i,
94
+ itemWidth,
95
+ itemHeight,
96
+ dynamicNumColumns,
97
+ containerPadding,
98
+ gap,
99
+ });
100
+ return {
101
+ key: d.key,
102
+ pos: {
103
+ x: useSharedValue(x),
104
+ y: useSharedValue(y),
105
+ active: useSharedValue(0),
106
+ },
107
+ };
108
+ });
109
+
110
+ const positions = useMemo(() => {
111
+ const obj: Record<string, (typeof positionsArray)[number]["pos"]> = {};
112
+
113
+ positionsArray.forEach(({ key, pos }) => {
114
+ obj[key ?? `key-${Math.random().toString(36).slice(2)}`] = pos;
115
+ });
116
+
117
+ return obj;
118
+ }, [positionsArray]);
119
+
120
+ const deleteItem = (key: string) => {
121
+ setOrderState((prev) => prev.filter((k) => k !== key));
122
+ order.value = order.value.filter((k) => k !== key);
123
+ onOrderChange?.([...order.value]);
124
+ // Call onDelete callback if provided (for both delete button and drop-to-delete)
125
+ if (onDelete) {
126
+ onDelete(key);
127
+ }
128
+ };
129
+
130
+ useDerivedValue(() => {
131
+ order.value.forEach((key, i) => {
132
+ if (activeKey.value === key) return; // ⬅️ do not layout the active tile
133
+
134
+ const p = positions[key];
135
+ if (!p) return;
136
+
137
+ const displayIndex = reverse ? order.value.length - 1 - i : i;
138
+
139
+ const { x, y } = indexToXY({
140
+ index: displayIndex,
141
+ itemWidth,
142
+ itemHeight,
143
+ dynamicNumColumns,
144
+ containerPadding,
145
+ gap,
146
+ });
147
+
148
+ const scale = Math.min(itemWidth, itemHeight) / 100; // 100px baseline
149
+
150
+ const damping = 18 * scale;
151
+ const stiffness = 240 * scale;
152
+ const mass = Math.max(0.1, scale); // helps stability for tiny items
153
+
154
+ p.x.value = withSpring(x, { damping, stiffness, mass });
155
+ p.y.value = withSpring(y, { damping, stiffness, mass });
156
+ });
157
+ });
158
+
159
+ // Layout of the ScrollView (viewport) — height we compare against for edge-scrolling
160
+ const onLayoutScrollView = (e: LayoutChangeEvent) => {
161
+ viewportH.value = e.nativeEvent.layout.height;
162
+ };
163
+
164
+ // Layout of the content view — width used to compute columns
165
+ const onLayoutContent = (e: LayoutChangeEvent) => {
166
+ contentW.value = e.nativeEvent.layout.width;
167
+ contentH.value = e.nativeEvent.layout.height;
168
+
169
+ if (numColumns) {
170
+ dynamicNumColumns.value = numColumns;
171
+ } else {
172
+ const possibleCols = Math.floor(
173
+ (e.nativeEvent.layout.width - containerPadding * 2 + gap) /
174
+ (itemWidth + gap)
175
+ );
176
+ dynamicNumColumns.value = Math.max(1, possibleCols);
177
+ }
178
+ };
179
+
180
+ const deleteComponentPosition = useSharedValue<{
181
+ x: number;
182
+ y: number;
183
+ width: number;
184
+ height: number;
185
+ } | null>(null);
186
+
187
+ const composed = Gesture.Simultaneous(
188
+ PanWithLongPress({
189
+ contentH,
190
+ order,
191
+ dynamicNumColumns,
192
+ activeKey,
193
+ offsetX,
194
+ offsetY,
195
+ startX,
196
+ startY,
197
+ dragMode,
198
+ positions,
199
+ itemsByKey,
200
+ itemWidth,
201
+ itemHeight,
202
+ containerPadding,
203
+ gap,
204
+ setOrderState,
205
+ onDragEnd,
206
+ onOrderChange,
207
+ scrollViewRef,
208
+ scrollOffset,
209
+ viewportH,
210
+ longPressMs,
211
+ scrollSpeed,
212
+ scrollThreshold,
213
+ reverse,
214
+ deleteComponentPosition,
215
+ deleteItem,
216
+ contentPaddingBottom,
217
+ })
218
+ );
219
+
220
+ return {
221
+ itemsByKey,
222
+ orderState,
223
+ dragMode,
224
+ anyItemInDeleteMode,
225
+ composed,
226
+ dynamicNumColumns,
227
+ onLayoutContent,
228
+ onLayoutScrollView,
229
+ positions,
230
+ onScroll,
231
+ childArray,
232
+ order,
233
+ deleteItem,
234
+ deleteComponentPosition,
235
+ };
236
+ }