react-native-smart-grid 0.1.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 (80) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +554 -0
  3. package/lib/module/components/DragLayer.js +71 -0
  4. package/lib/module/components/DragLayer.js.map +1 -0
  5. package/lib/module/components/DraggableTile.js +79 -0
  6. package/lib/module/components/DraggableTile.js.map +1 -0
  7. package/lib/module/components/GhostTile.js +37 -0
  8. package/lib/module/components/GhostTile.js.map +1 -0
  9. package/lib/module/components/GridTile.js +25 -0
  10. package/lib/module/components/GridTile.js.map +1 -0
  11. package/lib/module/components/ResizeHandle.js +72 -0
  12. package/lib/module/components/ResizeHandle.js.map +1 -0
  13. package/lib/module/components/SmartGrid.js +363 -0
  14. package/lib/module/components/SmartGrid.js.map +1 -0
  15. package/lib/module/context/GridDragContext.js +130 -0
  16. package/lib/module/context/GridDragContext.js.map +1 -0
  17. package/lib/module/engine/GridEngine.js +148 -0
  18. package/lib/module/engine/GridEngine.js.map +1 -0
  19. package/lib/module/engine/autoArrange.js +54 -0
  20. package/lib/module/engine/autoArrange.js.map +1 -0
  21. package/lib/module/engine/collisions.js +67 -0
  22. package/lib/module/engine/collisions.js.map +1 -0
  23. package/lib/module/hooks/useTileGesture.js +62 -0
  24. package/lib/module/hooks/useTileGesture.js.map +1 -0
  25. package/lib/module/index.js +9 -0
  26. package/lib/module/index.js.map +1 -0
  27. package/lib/module/layout/LayoutCalculator.js +29 -0
  28. package/lib/module/layout/LayoutCalculator.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/module/utils/pixelToGrid.js +22 -0
  33. package/lib/module/utils/pixelToGrid.js.map +1 -0
  34. package/lib/typescript/package.json +1 -0
  35. package/lib/typescript/src/components/DragLayer.d.ts +11 -0
  36. package/lib/typescript/src/components/DragLayer.d.ts.map +1 -0
  37. package/lib/typescript/src/components/DraggableTile.d.ts +14 -0
  38. package/lib/typescript/src/components/DraggableTile.d.ts.map +1 -0
  39. package/lib/typescript/src/components/GhostTile.d.ts +9 -0
  40. package/lib/typescript/src/components/GhostTile.d.ts.map +1 -0
  41. package/lib/typescript/src/components/GridTile.d.ts +9 -0
  42. package/lib/typescript/src/components/GridTile.d.ts.map +1 -0
  43. package/lib/typescript/src/components/ResizeHandle.d.ts +9 -0
  44. package/lib/typescript/src/components/ResizeHandle.d.ts.map +1 -0
  45. package/lib/typescript/src/components/SmartGrid.d.ts +214 -0
  46. package/lib/typescript/src/components/SmartGrid.d.ts.map +1 -0
  47. package/lib/typescript/src/context/GridDragContext.d.ts +44 -0
  48. package/lib/typescript/src/context/GridDragContext.d.ts.map +1 -0
  49. package/lib/typescript/src/engine/GridEngine.d.ts +35 -0
  50. package/lib/typescript/src/engine/GridEngine.d.ts.map +1 -0
  51. package/lib/typescript/src/engine/autoArrange.d.ts +4 -0
  52. package/lib/typescript/src/engine/autoArrange.d.ts.map +1 -0
  53. package/lib/typescript/src/engine/collisions.d.ts +3 -0
  54. package/lib/typescript/src/engine/collisions.d.ts.map +1 -0
  55. package/lib/typescript/src/hooks/useTileGesture.d.ts +13 -0
  56. package/lib/typescript/src/hooks/useTileGesture.d.ts.map +1 -0
  57. package/lib/typescript/src/index.d.ts +10 -0
  58. package/lib/typescript/src/index.d.ts.map +1 -0
  59. package/lib/typescript/src/layout/LayoutCalculator.d.ts +15 -0
  60. package/lib/typescript/src/layout/LayoutCalculator.d.ts.map +1 -0
  61. package/lib/typescript/src/types.d.ts +105 -0
  62. package/lib/typescript/src/types.d.ts.map +1 -0
  63. package/lib/typescript/src/utils/pixelToGrid.d.ts +9 -0
  64. package/lib/typescript/src/utils/pixelToGrid.d.ts.map +1 -0
  65. package/package.json +161 -0
  66. package/src/components/DragLayer.tsx +71 -0
  67. package/src/components/DraggableTile.tsx +88 -0
  68. package/src/components/GhostTile.tsx +42 -0
  69. package/src/components/GridTile.tsx +27 -0
  70. package/src/components/ResizeHandle.tsx +74 -0
  71. package/src/components/SmartGrid.tsx +506 -0
  72. package/src/context/GridDragContext.tsx +191 -0
  73. package/src/engine/GridEngine.ts +148 -0
  74. package/src/engine/autoArrange.ts +59 -0
  75. package/src/engine/collisions.ts +87 -0
  76. package/src/hooks/useTileGesture.ts +88 -0
  77. package/src/index.tsx +29 -0
  78. package/src/layout/LayoutCalculator.ts +50 -0
  79. package/src/types.ts +113 -0
  80. package/src/utils/pixelToGrid.ts +31 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Admin
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,554 @@
1
+ # react-native-smart-grid
2
+
3
+ A draggable, variable-sized tile grid for React Native. Think iOS home screen meets Pinterest meets Trello — with collision detection, auto-arrange, multi-select, and smooth spring animations.
4
+
5
+ > **Nothing like this exists in the RN ecosystem.** Every other grid library uses uniform tile sizes. This one doesn't.
6
+
7
+ <!-- Add a GIF demo here before publishing -->
8
+
9
+ ## Features
10
+
11
+ - **Variable-sized tiles** — 1×1, 1×2, 2×2, 2×4, any grid unit combination
12
+ - **Drag to reorder** — long press to lift, pan to move, spring to land
13
+ - **Collision detection** — push or swap modes
14
+ - **Auto-arrange** — bin-packing algorithm, callable via ref
15
+ - **Gravity** — tiles compact upward or leftward after every drop
16
+ - **Resize handles** — drag the corner to resize any tile in edit mode
17
+ - **Multi-select** — long press enters selection mode; tap to add/remove tiles
18
+ - **Serialization** — save and restore layouts, storage-agnostic
19
+ - **Virtualized** — only renders tiles in the viewport, handles thousands of items
20
+ - **Haptic callbacks** — bring your own haptics library, zero dependencies added
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ npm install react-native-smart-grid
28
+ # or
29
+ yarn add react-native-smart-grid
30
+ ```
31
+
32
+ ### Peer dependencies
33
+
34
+ ```sh
35
+ npm install react-native-gesture-handler react-native-reanimated
36
+ ```
37
+
38
+ Follow the setup guides for each:
39
+ - [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation)
40
+ - [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started)
41
+
42
+ Wrap your app root (or at minimum the screen containing `SmartGrid`) with `GestureHandlerRootView`:
43
+
44
+ ```tsx
45
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
46
+
47
+ export default function App() {
48
+ return (
49
+ <GestureHandlerRootView style={{ flex: 1 }}>
50
+ {/* your app */}
51
+ </GestureHandlerRootView>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Quick start
59
+
60
+ ```tsx
61
+ import { useState } from 'react';
62
+ import { Text, View } from 'react-native';
63
+ import { SmartGrid } from 'react-native-smart-grid';
64
+ import type { Tile, LayoutItem } from 'react-native-smart-grid';
65
+
66
+ type CardData = { label: string; color: string };
67
+
68
+ // Simplest form — no position, no size. Every tile defaults to 1×1 and
69
+ // is auto-placed in order using bin-packing.
70
+ const TILES: Tile<CardData>[] = [
71
+ { id: '1', data: { label: 'Music', color: '#6366f1' } },
72
+ { id: '2', data: { label: 'Photos', color: '#f59e0b' } },
73
+ { id: '3', data: { label: 'Notes', color: '#10b981' } },
74
+ { id: '4', data: { label: 'Calendar', color: '#ef4444' } },
75
+ ];
76
+
77
+ export default function App() {
78
+ const [tiles, setTiles] = useState(TILES);
79
+
80
+ function handleLayoutChange(layout: LayoutItem[]) {
81
+ setTiles(prev =>
82
+ prev.map(t => {
83
+ const updated = layout.find(l => l.id === t.id);
84
+ return updated ? { ...t, ...updated } : t;
85
+ })
86
+ );
87
+ }
88
+
89
+ return (
90
+ <SmartGrid
91
+ data={tiles}
92
+ columns={4}
93
+ rowHeight={100}
94
+ gap={8}
95
+ padding={12}
96
+ onLayoutChange={handleLayoutChange}
97
+ renderTile={({ item, isActive }) => (
98
+ <View style={{
99
+ flex: 1,
100
+ backgroundColor: item.data.color,
101
+ borderRadius: 12,
102
+ opacity: isActive ? 0.4 : 1,
103
+ }}>
104
+ <Text style={{ color: '#fff', padding: 8 }}>{item.data.label}</Text>
105
+ </View>
106
+ )}
107
+ />
108
+ );
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Tile data model
115
+
116
+ `position` and `size` are both optional. SmartGrid auto-places any tile that is missing either.
117
+
118
+ ```ts
119
+ // ── Option 1: explicit position + size ────────────────────────────────────────
120
+ // Full control. Use this when restoring a saved layout from storage/server.
121
+ const tiles: Tile<MyData>[] = [
122
+ { id: '1', position: { x: 0, y: 0 }, size: { w: 2, h: 2 }, data: { label: 'Large' } },
123
+ { id: '2', position: { x: 2, y: 0 }, size: { w: 2, h: 1 }, data: { label: 'Wide' } },
124
+ { id: '3', position: { x: 2, y: 1 }, size: { w: 1, h: 1 }, data: { label: 'Small' } },
125
+ ];
126
+
127
+ // ── Option 2: size only, no position ──────────────────────────────────────────
128
+ // Grid auto-places each tile using bin-packing. Good when you know the sizes
129
+ // but don't care where tiles land initially.
130
+ const tiles: Tile<MyData>[] = [
131
+ { id: '1', size: { w: 2, h: 2 }, data: { label: 'Large' } },
132
+ { id: '2', size: { w: 2, h: 1 }, data: { label: 'Wide' } },
133
+ { id: '3', size: { w: 1, h: 1 }, data: { label: 'Small' } },
134
+ ];
135
+
136
+ // ── Option 3: no position, no size ────────────────────────────────────────────
137
+ // Simplest form. Every tile defaults to 1×1 and is auto-placed in order.
138
+ const tiles: Tile<MyData>[] = [
139
+ { id: '1', data: { label: 'Music' } },
140
+ { id: '2', data: { label: 'Photos' } },
141
+ { id: '3', data: { label: 'Notes' } },
142
+ ];
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Props
148
+
149
+ | Prop | Type | Default | Description |
150
+ |---|---|---|---|
151
+ | `data` | `Tile<TData>[]` | **required** | Array of tiles. `position` and `size` are optional. |
152
+ | `renderTile` | `(info: RenderTileInfo) => ReactNode` | **required** | Render function for each tile. |
153
+ | `columns` | `number` | `4` | Number of grid columns. |
154
+ | `rowHeight` | `number` | `100` | Height of one grid row in pixels. |
155
+ | `gap` | `number` | `8` | Gap between tiles in pixels. |
156
+ | `padding` | `number` | `8` | Outer padding of the grid in pixels. |
157
+ | `collisionBehavior` | `'push' \| 'swap'` | `'push'` | How dropped tiles interact with others at the target position. |
158
+ | `gravity` | `'none' \| 'up' \| 'left'` | `'none'` | Compact tiles toward origin after each drop. |
159
+ | `isEditing` | `boolean` | `false` | Show resize handle on each tile's bottom-right corner. |
160
+ | `draggable` | `boolean` | `true` | Master switch — `false` disables drag on all tiles. |
161
+ | `selectable` | `boolean` | `true` | Master switch — `false` disables selection on all tiles. |
162
+ | `multiSelect` | `boolean` | `true` | `false` switches to single-select (new selection replaces old). |
163
+ | `onLayoutChange` | `(layout: LayoutItem[]) => void` | — | Fired after every drag, drop, or resize. Merge into your state. |
164
+ | `onTilePress` | `(tile: Tile) => void` | — | Quick tap (no drag). Open detail views, folders, etc. |
165
+ | `onTileDragStart` | `(tile: Tile) => void` | — | Fired when the 300ms long-press activates drag. |
166
+ | `onTileDrop` | `(tile, position) => void` | — | Fired when a tile lands at a new position. |
167
+ | `onTileResize` | `(tile, newSize) => void` | — | Fired when a tile is resized via the handle. |
168
+ | `onSelectionChange` | `(ids: string[]) => void` | — | Fired whenever the selection array changes. |
169
+ | `onHaptic` | `(event: HapticEvent) => void` | — | Fired at pick-up, snap, drop, and resize moments. |
170
+
171
+ ---
172
+
173
+ ## Ref API
174
+
175
+ ```tsx
176
+ import { useRef } from 'react';
177
+ import type { SmartGridRef } from 'react-native-smart-grid';
178
+
179
+ const gridRef = useRef<SmartGridRef>(null);
180
+
181
+ <SmartGrid ref={gridRef} ... />
182
+ ```
183
+
184
+ | Method | Description |
185
+ |---|---|
186
+ | `autoArrange()` | Re-packs all tiles using bin-packing (largest first). Fires `onLayoutChange`. |
187
+ | `serializeLayout()` | Returns `LayoutItem[]` — save anywhere, no `data` field included. |
188
+ | `restoreLayout(layout)` | Restores a previously serialized layout. Fires `onLayoutChange`. |
189
+ | `clearSelection()` | Clears the selection array. Fires `onSelectionChange`. |
190
+ | `setSelection(ids)` | Programmatically sets the selection. Fires `onSelectionChange`. |
191
+
192
+ ---
193
+
194
+ ## Usage examples
195
+
196
+ ### Collision behavior
197
+
198
+ `'push'` displaces tiles to the next free slot. `'swap'` exchanges the dragged tile with the one at the drop center.
199
+
200
+ ```tsx
201
+ // Default — displaced tiles cascade to the next available space
202
+ <SmartGrid collisionBehavior="push" ... />
203
+
204
+ // Drop center tile and dragged tile switch places, others untouched
205
+ <SmartGrid collisionBehavior="swap" ... />
206
+ ```
207
+
208
+ ---
209
+
210
+ ### Gravity
211
+
212
+ Automatically compacts the layout after every drop.
213
+
214
+ ```tsx
215
+ // No compaction — tiles stay exactly where dropped
216
+ <SmartGrid gravity="none" ... />
217
+
218
+ // Tiles slide upward to fill empty rows
219
+ <SmartGrid gravity="up" ... />
220
+
221
+ // Tiles slide leftward to fill empty columns
222
+ <SmartGrid gravity="left" ... />
223
+ ```
224
+
225
+ ---
226
+
227
+ ### Edit mode and resize
228
+
229
+ ```tsx
230
+ const [isEditing, setIsEditing] = useState(false);
231
+
232
+ <Button title={isEditing ? 'Done' : 'Edit'} onPress={() => setIsEditing(e => !e)} />
233
+
234
+ <SmartGrid
235
+ isEditing={isEditing}
236
+ onTileResize={(tile, newSize) => {
237
+ console.log(`${tile.id} resized to ${newSize.w}×${newSize.h}`);
238
+ }}
239
+ onLayoutChange={handleLayoutChange}
240
+ ...
241
+ />
242
+ ```
243
+
244
+ Use `locked` on a tile to hide its resize handle while still allowing drag:
245
+
246
+ ```ts
247
+ { id: 'header', size: { w: 4, h: 1 }, data: { label: 'Header' }, locked: true }
248
+ ```
249
+
250
+ ---
251
+
252
+ ### Tap to open
253
+
254
+ ```tsx
255
+ <SmartGrid
256
+ onTilePress={(tile) => {
257
+ navigation.navigate('Detail', { id: tile.id, data: tile.data });
258
+ }}
259
+ renderTile={({ item }) => (
260
+ <View style={styles.card}>
261
+ <Text>{item.data.label}</Text>
262
+ </View>
263
+ )}
264
+ ...
265
+ />
266
+ ```
267
+
268
+ `onTilePress` fires on a quick tap (< 200ms). It is suppressed while a selection is active — tapping a tile in selection mode toggles it instead.
269
+
270
+ ---
271
+
272
+ ### Selection — multi-select (default)
273
+
274
+ Long press a tile to enter selection mode. While selected, tapping any tile adds or removes it. A real drag-and-drop clears the selection.
275
+
276
+ ```tsx
277
+ const [selectedIds, setSelectedIds] = useState<string[]>([]);
278
+ const gridRef = useRef<SmartGridRef>(null);
279
+
280
+ <SmartGrid
281
+ ref={gridRef}
282
+ onSelectionChange={(ids) => setSelectedIds(ids)}
283
+ renderTile={({ item, isSelected }) => (
284
+ <View style={[styles.card, isSelected && styles.cardSelected]}>
285
+ <Text>{item.data.label}</Text>
286
+ {isSelected && (
287
+ <TouchableOpacity onPress={() => deleteTile(item.id)}>
288
+ <Text>✕ Delete</Text>
289
+ </TouchableOpacity>
290
+ )}
291
+ </View>
292
+ )}
293
+ ...
294
+ />
295
+
296
+ {selectedIds.length > 0 && (
297
+ <Button
298
+ title={`Delete ${selectedIds.length} tiles`}
299
+ onPress={() => {
300
+ setTiles(prev => prev.filter(t => !selectedIds.includes(t.id)));
301
+ gridRef.current?.clearSelection();
302
+ }}
303
+ />
304
+ )}
305
+ ```
306
+
307
+ ---
308
+
309
+ ### Selection — single-select
310
+
311
+ ```tsx
312
+ const [activeId, setActiveId] = useState<string | null>(null);
313
+
314
+ <SmartGrid
315
+ multiSelect={false}
316
+ onSelectionChange={([id]) => setActiveId(id ?? null)}
317
+ renderTile={({ item, isSelected }) => (
318
+ <View style={[styles.card, isSelected && styles.cardActive]}>
319
+ <Text>{item.data.label}</Text>
320
+ </View>
321
+ )}
322
+ ...
323
+ />
324
+ ```
325
+
326
+ ---
327
+
328
+ ### Programmatic selection
329
+
330
+ ```tsx
331
+ const gridRef = useRef<SmartGridRef>(null);
332
+
333
+ // Select specific tiles
334
+ gridRef.current?.setSelection(['tile-1', 'tile-3']);
335
+
336
+ // Clear all
337
+ gridRef.current?.clearSelection();
338
+ ```
339
+
340
+ ---
341
+
342
+ ### Disable drag, keep selection
343
+
344
+ When `draggable={false}`, tiles can still be long-pressed to enter selection. Selection fires **immediately** at the long-press threshold (300ms) rather than on release — identical to how the iOS Photos app behaves.
345
+
346
+ ```tsx
347
+ <SmartGrid
348
+ draggable={false} // tiles stay in place
349
+ onSelectionChange={(ids) => console.log('selected:', ids)}
350
+ renderTile={({ item, isSelected }) => (
351
+ <View style={[styles.card, isSelected && styles.cardSelected]}>
352
+ <Text>{item.data.label}</Text>
353
+ </View>
354
+ )}
355
+ ...
356
+ />
357
+ ```
358
+
359
+ ---
360
+
361
+ ### Grid-level switches
362
+
363
+ Master switches that override all per-tile flags.
364
+
365
+ ```tsx
366
+ // View-only — nothing moves, nothing selects
367
+ <SmartGrid draggable={false} selectable={false} ... />
368
+
369
+ // Locked layout — tiles visible, no interaction
370
+ <SmartGrid draggable={false} selectable={false} ... />
371
+
372
+ // Selection off — long press does nothing, onTilePress still fires on tap
373
+ <SmartGrid selectable={false} onTilePress={openDetail} ... />
374
+ ```
375
+
376
+ ---
377
+
378
+ ### Per-tile draggable / selectable
379
+
380
+ Fine-grained control on individual tiles. Grid-level switches take priority when set to `false`.
381
+
382
+ ```tsx
383
+ const tiles: Tile<MyData>[] = [
384
+ // Normal tile — draggable and selectable
385
+ { id: '1', data: { label: 'Drag me' } },
386
+
387
+ // Pinned tile — stays in place, cannot be dragged
388
+ { id: '2', data: { label: 'Pinned' }, draggable: false },
389
+
390
+ // Info tile — long press fires onTilePress instead of entering selection
391
+ { id: '3', data: { label: 'Info' }, selectable: false },
392
+
393
+ // Fully locked — no drag, no resize
394
+ { id: '4', data: { label: 'Header' }, locked: true },
395
+ ];
396
+ ```
397
+
398
+ ---
399
+
400
+ ### Auto-arrange
401
+
402
+ ```tsx
403
+ const gridRef = useRef<SmartGridRef>(null);
404
+
405
+ <Button
406
+ title="Auto-arrange"
407
+ onPress={() => gridRef.current?.autoArrange()}
408
+ />
409
+
410
+ <SmartGrid ref={gridRef} onLayoutChange={handleLayoutChange} ... />
411
+ ```
412
+
413
+ `autoArrange()` re-packs all tiles using a largest-first bin-packing algorithm and fires `onLayoutChange` with the new positions.
414
+
415
+ ---
416
+
417
+ ### Saving and restoring layouts
418
+
419
+ `serializeLayout` returns a plain array of `{ id, position, size }` — no `data` field. Save it anywhere.
420
+
421
+ ```tsx
422
+ const gridRef = useRef<SmartGridRef>(null);
423
+
424
+ // Save to your backend or AsyncStorage
425
+ async function save() {
426
+ const layout = gridRef.current?.serializeLayout();
427
+ await AsyncStorage.setItem('grid-layout', JSON.stringify(layout));
428
+ }
429
+
430
+ // Restore on next launch
431
+ async function restore() {
432
+ const raw = await AsyncStorage.getItem('grid-layout');
433
+ if (raw) gridRef.current?.restoreLayout(JSON.parse(raw));
434
+ }
435
+ ```
436
+
437
+ When restoring, pass the saved layout back through your `data` state before mounting the grid:
438
+
439
+ ```tsx
440
+ // Merge saved geometry back into your tile objects on startup
441
+ const saved = await AsyncStorage.getItem('grid-layout');
442
+ const savedLayout: LayoutItem[] = saved ? JSON.parse(saved) : [];
443
+
444
+ const initialTiles = MY_TILES.map(t => {
445
+ const saved = savedLayout.find(l => l.id === t.id);
446
+ return saved ? { ...t, position: saved.position, size: saved.size } : t;
447
+ });
448
+ ```
449
+
450
+ ---
451
+
452
+ ### Haptics
453
+
454
+ The library fires `onHaptic` at the right moments but has no opinion on which haptics library you use.
455
+
456
+ ```tsx
457
+ import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
458
+
459
+ <SmartGrid
460
+ onHaptic={(event) => {
461
+ if (event === 'pick-up') ReactNativeHapticFeedback.trigger('impactMedium');
462
+ if (event === 'snap') ReactNativeHapticFeedback.trigger('selection');
463
+ if (event === 'drop') ReactNativeHapticFeedback.trigger('notificationSuccess');
464
+ if (event === 'resize') ReactNativeHapticFeedback.trigger('impactLight');
465
+ }}
466
+ ...
467
+ />
468
+ ```
469
+
470
+ | Event | When |
471
+ |---|---|
472
+ | `'pick-up'` | Long press activates (drag starts or tile selected) |
473
+ | `'snap'` | Ghost tile snaps to a new grid position mid-drag |
474
+ | `'drop'` | Tile is released |
475
+ | `'resize'` | Tile resize is committed |
476
+
477
+ ---
478
+
479
+ ### Size constraints
480
+
481
+ Limit how small or large a tile can be resized (only enforced when `isEditing={true}`).
482
+
483
+ ```tsx
484
+ const tiles: Tile<MyData>[] = [
485
+ {
486
+ id: '1',
487
+ size: { w: 2, h: 2 },
488
+ minSize: { w: 1, h: 1 }, // can shrink to 1×1
489
+ maxSize: { w: 4, h: 4 }, // can grow up to 4×4
490
+ data: { label: 'Resizable' },
491
+ },
492
+ {
493
+ id: '2',
494
+ size: { w: 2, h: 1 },
495
+ locked: true, // resize handle hidden entirely
496
+ data: { label: 'Fixed' },
497
+ },
498
+ ];
499
+ ```
500
+
501
+ ---
502
+
503
+ ## Types
504
+
505
+ ```ts
506
+ type Tile<TData = unknown> = {
507
+ id: string; // unique, stable across re-renders
508
+ position?: TilePosition; // { x, y } — omit to auto-place
509
+ size?: TileSize; // { w, h } in grid units — omit to default 1×1
510
+ data: TData; // your custom data, passed back to renderTile
511
+ locked?: boolean; // hides resize handle, prevents resize
512
+ minSize?: TileSize; // smallest allowed size (isEditing only)
513
+ maxSize?: TileSize; // largest allowed size (isEditing only)
514
+ draggable?: boolean; // false = tile cannot be dragged (default: true)
515
+ selectable?: boolean; // false = long press fires onTilePress (default: true)
516
+ };
517
+
518
+ type TileSize = { w: number; h: number }; // grid units
519
+ type TilePosition = { x: number; y: number }; // column / row index, 0-based
520
+
521
+ type LayoutItem = {
522
+ id: string;
523
+ position: TilePosition;
524
+ size: TileSize;
525
+ };
526
+
527
+ type RenderTileInfo<TData> = {
528
+ item: Tile<TData>; // the tile being rendered
529
+ isActive: boolean; // true while this tile is being dragged (shows placeholder)
530
+ isSelected: boolean; // true when this tile is in the selection array
531
+ };
532
+
533
+ type SmartGridRef = {
534
+ autoArrange: () => void;
535
+ serializeLayout: () => LayoutItem[];
536
+ restoreLayout: (layout: LayoutItem[]) => void;
537
+ clearSelection: () => void;
538
+ setSelection: (ids: string[]) => void;
539
+ };
540
+
541
+ type CollisionBehavior = 'push' | 'swap';
542
+ type Gravity = 'none' | 'up' | 'left';
543
+ type HapticEvent = 'pick-up' | 'snap' | 'drop' | 'resize';
544
+ ```
545
+
546
+ ---
547
+
548
+ ## Contributing
549
+
550
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
551
+
552
+ ## License
553
+
554
+ MIT
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ import { memo, useEffect } from 'react';
4
+ import { StyleSheet } from 'react-native';
5
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
6
+ import { useGridDrag } from "../context/GridDragContext.js";
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const LIFT_SPRING = {
9
+ damping: 15,
10
+ stiffness: 300,
11
+ mass: 0.4
12
+ };
13
+ function DragLayerInner({
14
+ tile: _tile,
15
+ initialRect,
16
+ children
17
+ }) {
18
+ const {
19
+ dragAbsX,
20
+ dragAbsY,
21
+ containerPageX,
22
+ containerPageY,
23
+ scrollYRef
24
+ } = useGridDrag();
25
+ const scale = useSharedValue(1);
26
+ const opacity = useSharedValue(0);
27
+ useEffect(() => {
28
+ scale.value = withSpring(1.06, LIFT_SPRING);
29
+ opacity.value = withSpring(1, LIFT_SPRING);
30
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
31
+
32
+ const animStyle = useAnimatedStyle(() => {
33
+ const x = dragAbsX.value - containerPageX.current - initialRect.width / 2;
34
+ const y = dragAbsY.value - containerPageY.current + scrollYRef.current - initialRect.height / 2;
35
+ return {
36
+ opacity: opacity.value,
37
+ transform: [{
38
+ translateX: x
39
+ }, {
40
+ translateY: y
41
+ }, {
42
+ scale: scale.value
43
+ }]
44
+ };
45
+ });
46
+ return /*#__PURE__*/_jsx(Animated.View, {
47
+ style: [styles.layer, {
48
+ width: initialRect.width,
49
+ height: initialRect.height
50
+ }, animStyle],
51
+ children: children
52
+ });
53
+ }
54
+ export const DragLayer = /*#__PURE__*/memo(DragLayerInner);
55
+ const styles = StyleSheet.create({
56
+ layer: {
57
+ position: 'absolute',
58
+ top: 0,
59
+ left: 0,
60
+ zIndex: 999,
61
+ shadowColor: '#000',
62
+ shadowOffset: {
63
+ width: 0,
64
+ height: 10
65
+ },
66
+ shadowOpacity: 0.4,
67
+ shadowRadius: 16,
68
+ elevation: 16
69
+ }
70
+ });
71
+ //# sourceMappingURL=DragLayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["memo","useEffect","StyleSheet","Animated","useAnimatedStyle","useSharedValue","withSpring","useGridDrag","jsx","_jsx","LIFT_SPRING","damping","stiffness","mass","DragLayerInner","tile","_tile","initialRect","children","dragAbsX","dragAbsY","containerPageX","containerPageY","scrollYRef","scale","opacity","value","animStyle","x","current","width","y","height","transform","translateX","translateY","View","style","styles","layer","DragLayer","create","position","top","left","zIndex","shadowColor","shadowOffset","shadowOpacity","shadowRadius","elevation"],"sourceRoot":"..\\..\\..\\src","sources":["components/DragLayer.tsx"],"mappings":";;AAAA,SAASA,IAAI,EAAEC,SAAS,QAAQ,OAAO;AACvC,SAASC,UAAU,QAAQ,cAAc;AACzC,OAAOC,QAAQ,IACbC,gBAAgB,EAChBC,cAAc,EACdC,UAAU,QACL,yBAAyB;AAChC,SAASC,WAAW,QAAQ,+BAA4B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAIzD,MAAMC,WAAW,GAAG;EAAEC,OAAO,EAAE,EAAE;EAAEC,SAAS,EAAE,GAAG;EAAEC,IAAI,EAAE;AAAI,CAAC;AAQ9D,SAASC,cAAcA,CAAQ;EAAEC,IAAI,EAAEC,KAAK;EAAEC,WAAW;EAAEC;AAAuB,CAAC,EAAE;EACnF,MAAM;IAAEC,QAAQ;IAAEC,QAAQ;IAAEC,cAAc;IAAEC,cAAc;IAAEC;EAAW,CAAC,GAAGhB,WAAW,CAAC,CAAC;EAExF,MAAMiB,KAAK,GAAGnB,cAAc,CAAC,CAAC,CAAC;EAC/B,MAAMoB,OAAO,GAAGpB,cAAc,CAAC,CAAC,CAAC;EAEjCJ,SAAS,CAAC,MAAM;IACduB,KAAK,CAACE,KAAK,GAAGpB,UAAU,CAAC,IAAI,EAAEI,WAAW,CAAC;IAC3Ce,OAAO,CAACC,KAAK,GAAGpB,UAAU,CAAC,CAAC,EAAEI,WAAW,CAAC;EAC5C,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;;EAER,MAAMiB,SAAS,GAAGvB,gBAAgB,CAAC,MAAM;IACvC,MAAMwB,CAAC,GAAGT,QAAQ,CAACO,KAAK,GAAGL,cAAc,CAACQ,OAAO,GAAGZ,WAAW,CAACa,KAAK,GAAG,CAAC;IACzE,MAAMC,CAAC,GACLX,QAAQ,CAACM,KAAK,GACdJ,cAAc,CAACO,OAAO,GACtBN,UAAU,CAACM,OAAO,GAClBZ,WAAW,CAACe,MAAM,GAAG,CAAC;IACxB,OAAO;MACLP,OAAO,EAAEA,OAAO,CAACC,KAAK;MACtBO,SAAS,EAAE,CAAC;QAAEC,UAAU,EAAEN;MAAE,CAAC,EAAE;QAAEO,UAAU,EAAEJ;MAAE,CAAC,EAAE;QAAEP,KAAK,EAAEA,KAAK,CAACE;MAAM,CAAC;IAC1E,CAAC;EACH,CAAC,CAAC;EAEF,oBACEjB,IAAA,CAACN,QAAQ,CAACiC,IAAI;IACZC,KAAK,EAAE,CACLC,MAAM,CAACC,KAAK,EACZ;MAAET,KAAK,EAAEb,WAAW,CAACa,KAAK;MAAEE,MAAM,EAAEf,WAAW,CAACe;IAAO,CAAC,EACxDL,SAAS,CACT;IAAAT,QAAA,EAEDA;EAAQ,CACI,CAAC;AAEpB;AAEA,OAAO,MAAMsB,SAAS,gBAAGxC,IAAI,CAACc,cAAc,CAA0B;AAEtE,MAAMwB,MAAM,GAAGpC,UAAU,CAACuC,MAAM,CAAC;EAC/BF,KAAK,EAAE;IACLG,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,MAAM,EAAE,GAAG;IACXC,WAAW,EAAE,MAAM;IACnBC,YAAY,EAAE;MAAEjB,KAAK,EAAE,CAAC;MAAEE,MAAM,EAAE;IAAG,CAAC;IACtCgB,aAAa,EAAE,GAAG;IAClBC,YAAY,EAAE,EAAE;IAChBC,SAAS,EAAE;EACb;AACF,CAAC,CAAC","ignoreList":[]}