panelui-native 0.49.0 → 0.51.1

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.
@@ -0,0 +1,806 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Sortable — a list whose rows can be dragged into a different order.
5
+ *
6
+ * The one list interaction nothing else in the library covers. `Swipe` acts on
7
+ * a row, `Tree` opens one, `Table` sorts every row at once by a column — none
8
+ * of them let a person say *this one goes above that one*, which is how a
9
+ * playlist, a task list, a set of form fields or a run of dashboard tiles is
10
+ * actually arranged.
11
+ *
12
+ * ```tsx
13
+ * const [tasks, setTasks] = useState(TASKS);
14
+ *
15
+ * <Sortable
16
+ * value={tasks.map((task) => task.id)}
17
+ * onReorder={(_, { from, to }) => setTasks((t) => reorderItems(t, from, to))}
18
+ * gap={8}
19
+ * >
20
+ * {tasks.map((task) => (
21
+ * <Sortable.Item key={task.id} id={task.id}>
22
+ * <Item variant="outline">
23
+ * <Item.Content>
24
+ * <Item.Title>{task.title}</Item.Title>
25
+ * </Item.Content>
26
+ * <Sortable.Handle />
27
+ * </Item>
28
+ * </Sortable.Item>
29
+ * ))}
30
+ * </Sortable>
31
+ * ```
32
+ *
33
+ * ## Nothing is ever moved in the tree
34
+ *
35
+ * The rows stay exactly where they were laid out and are pushed around with
36
+ * transforms. A row's offset is the difference between where its slot sits in
37
+ * the order being dragged and where it sits in the order that was rendered —
38
+ * one subtraction, on the UI thread, per row per frame. Reordering the
39
+ * children instead would mean React reconciling the whole list on every slot
40
+ * the finger crosses, which is the one thing a drag cannot afford.
41
+ *
42
+ * It also means the component never owns the order. It reports where a row was
43
+ * dropped and the list is the caller's to rearrange, because the caller is the
44
+ * only one who knows what the ids stand for — a `value` that disagreed with
45
+ * the children would put rows in places their content had not moved to.
46
+ *
47
+ * ## Heights are measured, not assumed
48
+ *
49
+ * Every row reports its own height, so a list of rows of different sizes lands
50
+ * in the right slots. A fixed row height would be one number to get wrong in
51
+ * every list that has a two-line row in it, and the measurement costs one
52
+ * layout pass on mount.
53
+ *
54
+ * ## Where the drop is reported
55
+ *
56
+ * `onReorder` fires when the row has finished settling, not when the finger
57
+ * lifts. Between those two moments the row is animating into a slot that the
58
+ * layout does not know about yet; re-rendering the list in the middle of that
59
+ * would relayout every row underneath it and the settling row would jump. By
60
+ * the time the callback runs the rows are already where the new order puts
61
+ * them, so the re-render that follows changes nothing on screen.
62
+ */
63
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
64
+ import { View } from 'react-native';
65
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
66
+ import Animated, { measure, runOnJS, scrollTo, useAnimatedRef, useAnimatedStyle, useFrameCallback, useReducedMotion, useScrollViewOffset, useSharedValue, withSpring } from 'react-native-reanimated';
67
+ import { useCSSVariable } from 'uniwind';
68
+ import { GripVerticalIcon, IconColorProvider } from "../../icons/index.js";
69
+ import { cn } from "../../utils/cn.js";
70
+ import { impactKnock, selectionTick } from "../../utils/haptics.js";
71
+
72
+ /**
73
+ * Rows getting out of the way of the one being carried. Quick, because they
74
+ * are answering a finger that has already moved — a neighbour that ambles into
75
+ * its new slot reads as the list struggling to keep up with the drag.
76
+ */
77
+ import { jsx as _jsx } from "react/jsx-runtime";
78
+ const DISPLACE = {
79
+ damping: 22,
80
+ stiffness: 300,
81
+ mass: 0.7
82
+ };
83
+
84
+ /**
85
+ * Settles a row into its slot. Stiffer and less bouncy than the library's
86
+ * overlay springs: a row that overshoots its slot reads as having landed in
87
+ * the wrong one and then corrected itself.
88
+ *
89
+ * The rest thresholds are loosened from the defaults on purpose. The drop is
90
+ * reported from this spring's completion, so the tolerance it finishes at is
91
+ * also how long the caller waits to hear about it — and the last hundredth of
92
+ * a point of travel is not something anybody can see.
93
+ */
94
+ const LAND = {
95
+ damping: 26,
96
+ stiffness: 260,
97
+ mass: 0.7,
98
+ restDisplacementThreshold: 0.5,
99
+ restSpeedThreshold: 2
100
+ };
101
+
102
+ /**
103
+ * Coming loose, and settling back. Deliberately faster than `LAND`: the row
104
+ * has to be back at its own size by the time it arrives, or it finishes the
105
+ * drop full-sized and then shrinks, which reads as two separate movements.
106
+ */
107
+ const LIFT = {
108
+ damping: 20,
109
+ stiffness: 400,
110
+ mass: 0.5
111
+ };
112
+
113
+ /**
114
+ * How much a lifted row grows. Enough to read as picked up over a full-width
115
+ * row, where a larger jump would push the row past the edges of the list it
116
+ * came out of.
117
+ */
118
+ const LIFT_SCALE = 1.05;
119
+
120
+ /**
121
+ * How far the finger must travel on a handle before the drag takes over. In
122
+ * line with the rest of the library's pans: below about this, a list inside a
123
+ * scroller cannot be scrolled by a finger that happened to land on a grip.
124
+ */
125
+ const HANDLE_SLOP = 10;
126
+
127
+ /** What starts a drag. */
128
+
129
+ /** Where a row ended up, alongside the order it produced. */
130
+
131
+ /**
132
+ * The same move applied to a list of your own.
133
+ *
134
+ * `onReorder` hands back the new order of the ids, but a list is rarely a list
135
+ * of ids — it is a list of the things they name. This does the move on that
136
+ * list, and returns a new array rather than sorting in place, because the one
137
+ * that mutated would be the one React had already decided was unchanged.
138
+ *
139
+ * ```tsx
140
+ * onReorder={(_, { from, to }) => setTasks((tasks) => reorderItems(tasks, from, to))}
141
+ * ```
142
+ *
143
+ * Indices outside the list are returned unchanged rather than throwing: a
144
+ * reorder is not worth crashing a screen over, and a list that did not move is
145
+ * the honest result of a move that had nowhere to go.
146
+ */
147
+ export function reorderItems(items, from, to) {
148
+ if (from === to) return [...items];
149
+ if (from < 0 || from >= items.length) return [...items];
150
+ if (to < 0 || to >= items.length) return [...items];
151
+ const next = [...items];
152
+ const moved = next.splice(from, 1);
153
+ next.splice(to, 0, ...moved);
154
+ return next;
155
+ }
156
+
157
+ /* -------------------------------------------------------------------------- */
158
+ /* Slot maths */
159
+ /* -------------------------------------------------------------------------- */
160
+
161
+ /**
162
+ * How far down the list a row's slot begins, given an order and the measured
163
+ * heights — the sum of everything above it plus one gap per row above it.
164
+ *
165
+ * A worklet, and deliberately a loop rather than a cached prefix sum: the
166
+ * lists this component is for are the ones a person is willing to drag through
167
+ * by hand, and a cache would be another thing to invalidate every time a row
168
+ * changed height.
169
+ */
170
+ function slotOffset(order, id, heights, gap) {
171
+ 'worklet';
172
+
173
+ let offset = 0;
174
+ for (let i = 0; i < order.length; i += 1) {
175
+ const at = order[i];
176
+ if (at === id) return offset;
177
+ offset += (at === undefined ? 0 : heights[at] ?? 0) + gap;
178
+ }
179
+ return offset;
180
+ }
181
+
182
+ /** The middle of the slot at `index`, in the same coordinates. */
183
+ function slotCenter(order, index, heights, gap) {
184
+ 'worklet';
185
+
186
+ let offset = 0;
187
+ for (let i = 0; i < index; i += 1) {
188
+ const at = order[i];
189
+ offset += (at === undefined ? 0 : heights[at] ?? 0) + gap;
190
+ }
191
+ const self = order[index];
192
+ return offset + (self === undefined ? 0 : heights[self] ?? 0) / 2;
193
+ }
194
+
195
+ /**
196
+ * Where the dragged row belongs now, given where its middle has reached.
197
+ *
198
+ * It walks outwards from the row's current slot and stops at the first
199
+ * neighbour it has *not* passed the middle of, rather than scanning the whole
200
+ * list for the nearest slot. The difference shows up with rows of unequal
201
+ * height: scanning can hand back a slot two places away that happens to be
202
+ * closer, which reads as the row skipping one.
203
+ */
204
+ function targetIndex(order, current, center, heights, gap) {
205
+ 'worklet';
206
+
207
+ if (center < slotCenter(order, current, heights, gap)) {
208
+ let target = current;
209
+ for (let i = current - 1; i >= 0; i -= 1) {
210
+ if (center >= slotCenter(order, i, heights, gap)) break;
211
+ target = i;
212
+ }
213
+ return target;
214
+ }
215
+ let target = current;
216
+ for (let i = current + 1; i < order.length; i += 1) {
217
+ if (center <= slotCenter(order, i, heights, gap)) break;
218
+ target = i;
219
+ }
220
+ return target;
221
+ }
222
+
223
+ /* -------------------------------------------------------------------------- */
224
+ /* Context */
225
+ /* -------------------------------------------------------------------------- */
226
+
227
+ const SortableRootContext = /*#__PURE__*/createContext(null);
228
+ function useSortableRoot(part) {
229
+ const context = useContext(SortableRootContext);
230
+ if (!context) throw new Error(`${part} must be used inside <Sortable>.`);
231
+ return context;
232
+ }
233
+ const SortableItemContext = /*#__PURE__*/createContext(null);
234
+
235
+ /**
236
+ * What the row being rendered knows about itself.
237
+ *
238
+ * For a handle of your own, or for a row that looks different while it is
239
+ * being carried — dimmed neighbours, a border, a shadow. `isActive` is a plain
240
+ * boolean and changes twice in a drag rather than sixty times a second: it is
241
+ * set when the row is lifted and cleared when it lands, and nothing between
242
+ * those two moments touches React at all.
243
+ */
244
+ export function useSortableItem() {
245
+ const context = useContext(SortableItemContext);
246
+ if (!context) throw new Error('useSortableItem must be used inside <Sortable.Item>.');
247
+ const {
248
+ id,
249
+ index,
250
+ isActive,
251
+ disabled
252
+ } = context;
253
+ return {
254
+ id,
255
+ index,
256
+ isActive,
257
+ disabled
258
+ };
259
+ }
260
+
261
+ /* -------------------------------------------------------------------------- */
262
+ /* Root */
263
+ /* -------------------------------------------------------------------------- */
264
+
265
+ /**
266
+ * The list, and everything the rows share.
267
+ *
268
+ * It renders one plain `View` in a column and nothing else. The rows are the
269
+ * caller's, laid out by flexbox in the order they were written; the component
270
+ * only ever adds a transform to them.
271
+ */
272
+ function SortableRoot({
273
+ className,
274
+ value,
275
+ children,
276
+ onReorder,
277
+ gap = 0,
278
+ activation = 'handle',
279
+ longPressDelay = 220,
280
+ haptics = true,
281
+ disabled = false,
282
+ scrollRef,
283
+ autoscrollThreshold = 72,
284
+ autoscrollSpeed = 8,
285
+ onDragStart,
286
+ onDragEnd,
287
+ ...props
288
+ }) {
289
+ const order = useSharedValue(value);
290
+ const rendered = useSharedValue(value);
291
+ const heights = useSharedValue({});
292
+ const activeId = useSharedValue(null);
293
+ const translate = useSharedValue(0);
294
+ const lift = useSharedValue(0);
295
+ const dragSeq = useSharedValue(0);
296
+ const fingerY = useSharedValue(0);
297
+ const [activeItem, setActiveItem] = useState(null);
298
+ const reducedMotion = useReducedMotion();
299
+
300
+ /*
301
+ * A joined key rather than the array: `value` is almost always a fresh
302
+ * `.map()` on every render, so depending on it directly would reset the
303
+ * order on every keystroke elsewhere on the screen. A null separator
304
+ * cannot appear in an id that came from anywhere real.
305
+ */
306
+ const key = value.join('\u0000');
307
+
308
+ /*
309
+ * The caller has applied the drop, so the rendered order is the new one and
310
+ * the transforms that were holding rows in their new slots are no longer
311
+ * needed — the layout puts them there now. Both orders are reset together,
312
+ * which is what makes the re-render after a drop invisible.
313
+ */
314
+ useEffect(() => {
315
+ order.value = value;
316
+ rendered.value = value;
317
+ translate.value = 0;
318
+ lift.value = 0;
319
+ // `value` is covered by `key`; depending on the array itself would fire
320
+ // this on every render of the screen around it.
321
+ // eslint-disable-next-line react-hooks/exhaustive-deps
322
+ }, [key, order, rendered, translate, lift]);
323
+ const indices = useMemo(() => {
324
+ const map = new Map();
325
+ value.forEach((id, index) => map.set(id, index));
326
+ return map;
327
+ // eslint-disable-next-line react-hooks/exhaustive-deps
328
+ }, [key]);
329
+ const indexOf = useCallback(id => indices.get(id) ?? -1, [indices]);
330
+ const measured = useCallback((id, height) => {
331
+ if (heights.value[id] === height) return;
332
+ heights.value = {
333
+ ...heights.value,
334
+ [id]: height
335
+ };
336
+ }, [heights]);
337
+
338
+ /* ---------------------------------------------------------------------- */
339
+ /* Autoscroll */
340
+ /* ---------------------------------------------------------------------- */
341
+
342
+ /*
343
+ * A ref of its own so `useScrollViewOffset` always has one to hold. It is
344
+ * never attached to anything, and the hook is happy with a ref that resolves
345
+ * to nothing — which is exactly the case where there is no scrolling to do.
346
+ */
347
+ const fallbackRef = useAnimatedRef();
348
+ const scrollOffset = useScrollViewOffset(scrollRef ?? fallbackRef);
349
+
350
+ /**
351
+ * Where the scroller stood when the drag began. The row follows the finger,
352
+ * and the finger is measured against the screen — so every point the list
353
+ * scrolls underneath it has to be added back, or a row would slide out from
354
+ * under the finger the moment the list started moving.
355
+ */
356
+ const scrollAtStart = useSharedValue(0);
357
+
358
+ /*
359
+ * A frame callback rather than something driven by the pan, because the
360
+ * finger held still at the edge is exactly when the scrolling has to keep
361
+ * happening — and a pan that is not moving sends no updates.
362
+ */
363
+ const autoscroll = useFrameCallback(() => {
364
+ 'worklet';
365
+
366
+ if (!scrollRef || activeId.value === null) return;
367
+ const view = measure(scrollRef);
368
+ if (!view) return;
369
+ const top = view.pageY + autoscrollThreshold;
370
+ const bottom = view.pageY + view.height - autoscrollThreshold;
371
+ const y = fingerY.value;
372
+ let intensity = 0;
373
+ if (y < top) intensity = (y - top) / autoscrollThreshold;else if (y > bottom) intensity = (y - bottom) / autoscrollThreshold;
374
+ if (intensity === 0) return;
375
+ const clamped = Math.max(-1, Math.min(1, intensity));
376
+ scrollTo(scrollRef, 0, scrollOffset.value + clamped * autoscrollSpeed, false);
377
+ }, false);
378
+
379
+ /*
380
+ * `setActive` is a JS-thread call, so the gesture reaches it through
381
+ * `runOnJS` rather than flipping a flag the callback would have to poll.
382
+ */
383
+ const setAutoscroll = useCallback(active => {
384
+ if (!scrollRef) return;
385
+ autoscroll.setActive(active);
386
+ }, [autoscroll, scrollRef]);
387
+
388
+ /* ---------------------------------------------------------------------- */
389
+ /* Lifting and landing */
390
+ /* ---------------------------------------------------------------------- */
391
+
392
+ const begin = useCallback(id => {
393
+ setActiveItem(id);
394
+ setAutoscroll(true);
395
+ if (haptics) impactKnock();
396
+ onDragStart?.(id);
397
+ }, [haptics, onDragStart, setAutoscroll]);
398
+
399
+ /**
400
+ * The row has landed. Only now is the drop reported: until the spring
401
+ * finished, the row was in a slot the layout knew nothing about, and a
402
+ * re-render in the middle of that would have relaid out every row under it
403
+ * while one of them was still moving.
404
+ */
405
+ const settled = useCallback(id => {
406
+ setActiveItem(null);
407
+ setAutoscroll(false);
408
+ onDragEnd?.(id);
409
+ const next = order.value;
410
+ const from = rendered.value.indexOf(id);
411
+ const to = next.indexOf(id);
412
+ if (from === to || from < 0 || to < 0) return;
413
+ onReorder?.([...next], {
414
+ id,
415
+ from,
416
+ to
417
+ });
418
+ }, [onDragEnd, onReorder, order, rendered, setAutoscroll]);
419
+
420
+ /**
421
+ * A move of whole slots, with no gesture behind it — the path a screen
422
+ * reader takes, and the one a keyboard would take if a phone had one. It
423
+ * reports the drop straight away: nothing is mid-flight, so there is no
424
+ * settling to wait for.
425
+ */
426
+ const step = useCallback((id, delta) => {
427
+ const current = rendered.value.indexOf(id);
428
+ if (current < 0) return;
429
+ const to = current + delta;
430
+ if (to < 0 || to >= rendered.value.length) return;
431
+ const next = reorderItems(rendered.value, current, to);
432
+ order.value = next;
433
+ if (haptics) selectionTick();
434
+ onReorder?.(next, {
435
+ id,
436
+ from: current,
437
+ to
438
+ });
439
+ }, [haptics, onReorder, order, rendered]);
440
+ const context = useMemo(() => ({
441
+ order,
442
+ rendered,
443
+ heights,
444
+ activeId,
445
+ translate,
446
+ lift,
447
+ dragSeq,
448
+ fingerY,
449
+ scrollOffset,
450
+ scrollAtStart,
451
+ gap,
452
+ disabled,
453
+ activation,
454
+ longPressDelay,
455
+ haptics,
456
+ reducedMotion,
457
+ activeItem,
458
+ indexOf,
459
+ measured,
460
+ begin,
461
+ settled,
462
+ step,
463
+ setAutoscroll
464
+ }), [order, rendered, heights, activeId, translate, lift, dragSeq, fingerY, scrollOffset, scrollAtStart, gap, disabled, activation, longPressDelay, haptics, reducedMotion, activeItem, indexOf, measured, begin, settled, step, setAutoscroll]);
465
+ return /*#__PURE__*/_jsx(SortableRootContext.Provider, {
466
+ value: context,
467
+ children: /*#__PURE__*/_jsx(View, {
468
+ accessibilityRole: "list",
469
+ className: cn('w-full', className),
470
+ style: gap > 0 ? {
471
+ gap
472
+ } : undefined,
473
+ ...props,
474
+ children: children
475
+ })
476
+ });
477
+ }
478
+ SortableRoot.displayName = 'Sortable';
479
+
480
+ /* -------------------------------------------------------------------------- */
481
+ /* Item */
482
+ /* -------------------------------------------------------------------------- */
483
+
484
+ /**
485
+ * One row.
486
+ *
487
+ * It measures itself, carries its own transform, and holds the pan — which is
488
+ * either wrapped around the whole row or handed to a `Sortable.Handle` inside
489
+ * it, depending on what lifts a row in this list.
490
+ */
491
+ function SortableItem({
492
+ className,
493
+ activeClassName,
494
+ id,
495
+ children,
496
+ disabled = false,
497
+ ...props
498
+ }) {
499
+ const root = useSortableRoot('Sortable.Item');
500
+ const {
501
+ order,
502
+ rendered,
503
+ heights,
504
+ activeId,
505
+ translate,
506
+ lift,
507
+ dragSeq,
508
+ fingerY,
509
+ scrollOffset,
510
+ scrollAtStart,
511
+ gap,
512
+ activation,
513
+ longPressDelay,
514
+ haptics,
515
+ reducedMotion,
516
+ activeItem,
517
+ indexOf,
518
+ measured,
519
+ begin,
520
+ settled,
521
+ step
522
+ } = root;
523
+
524
+ /** A row is undraggable if either it or the whole list says so. */
525
+ const locked = root.disabled || disabled;
526
+ const index = indexOf(id);
527
+ const isActive = activeItem === id;
528
+ const tick = useCallback(() => selectionTick(), []);
529
+
530
+ /*
531
+ * Read off a ref inside the worklets rather than captured by them: `begin`
532
+ * and `settled` change identity whenever the caller's callbacks do, and a
533
+ * gesture rebuilt mid-drag is a gesture that drops the drag.
534
+ */
535
+ const handlers = useRef({
536
+ begin,
537
+ settled,
538
+ tick
539
+ });
540
+ handlers.current = {
541
+ begin,
542
+ settled,
543
+ tick
544
+ };
545
+ const notifyBegin = useCallback(rowId => {
546
+ handlers.current.begin(rowId);
547
+ }, []);
548
+ const notifySettled = useCallback(rowId => {
549
+ handlers.current.settled(rowId);
550
+ }, []);
551
+ const notifyTick = useCallback(() => {
552
+ handlers.current.tick();
553
+ }, []);
554
+ const pan = useMemo(() => {
555
+ const gesture = Gesture.Pan().enabled(!locked).onStart(event => {
556
+ activeId.value = id;
557
+ translate.value = 0;
558
+ order.value = [...rendered.value];
559
+ fingerY.value = event.absoluteY;
560
+ /*
561
+ * Claims the row: a landing spring left over from the previous drop
562
+ * finds this number changed and leaves the row alone rather than
563
+ * putting down one that has just been picked back up.
564
+ */
565
+ dragSeq.value += 1;
566
+ /*
567
+ * Left at rest when motion is turned down, so the row does not grow.
568
+ * The surface and the shadow it gets in the same moment are not
569
+ * motion and stay either way — they are what stop the lifted row
570
+ * being see-through, which is not a preference.
571
+ */
572
+ lift.value = reducedMotion ? 0 : withSpring(1, LIFT);
573
+ /*
574
+ * Taken here rather than on the JS side: `notifyBegin` lands a frame
575
+ * or more later, and by then a fast flick may already have moved the
576
+ * scroller — which would be read as the finger having moved.
577
+ */
578
+ scrollAtStart.value = scrollOffset.value;
579
+ runOnJS(notifyBegin)(id);
580
+ }).onUpdate(event => {
581
+ if (activeId.value !== id) return;
582
+
583
+ /*
584
+ * The pan reports the finger against the screen, so every point the
585
+ * list has scrolled underneath it since the lift has to be added
586
+ * back — otherwise a row slides out from under the finger the moment
587
+ * a drag at the edge starts scrolling.
588
+ */
589
+ translate.value = event.translationY + (scrollOffset.value - scrollAtStart.value);
590
+ fingerY.value = event.absoluteY;
591
+ const list = order.value;
592
+ const map = heights.value;
593
+ const current = list.indexOf(id);
594
+ if (current < 0) return;
595
+ const top = slotOffset(rendered.value, id, map, gap) + translate.value;
596
+ const center = top + (map[id] ?? 0) / 2;
597
+ const to = targetIndex(list, current, center, map, gap);
598
+ if (to === current) return;
599
+ const next = [...list];
600
+ next.splice(current, 1);
601
+ next.splice(to, 0, id);
602
+ order.value = next;
603
+ if (haptics) runOnJS(notifyTick)();
604
+ }).onFinalize(() => {
605
+ if (activeId.value !== id) return;
606
+
607
+ /*
608
+ * The row springs to where its new slot sits *in the old layout* —
609
+ * the layout has not changed yet and will not until the drop is
610
+ * reported, which is what the callback below does once the row has
611
+ * stopped moving.
612
+ */
613
+ const landing = slotOffset(order.value, id, heights.value, gap) - slotOffset(rendered.value, id, heights.value, gap);
614
+
615
+ /*
616
+ * Released here rather than once the row has landed, so the shrink and
617
+ * the drop are one movement. `LIFT` is the faster spring of the two,
618
+ * so the row is back at its own size a little before it arrives.
619
+ */
620
+ dragSeq.value += 1;
621
+ const seq = dragSeq.value;
622
+ lift.value = reducedMotion ? 0 : withSpring(0, LIFT);
623
+
624
+ /*
625
+ * Runs whether or not the spring finished. An interrupted spring used
626
+ * to leave `activeId` set, which left the row lifted for good and the
627
+ * drop unreported; `seq` is what tells the two cases apart, because
628
+ * the only interruption that should be ignored is the row being picked
629
+ * up again.
630
+ */
631
+ const land = () => {
632
+ 'worklet';
633
+
634
+ if (dragSeq.value !== seq) return;
635
+ activeId.value = null;
636
+ runOnJS(notifySettled)(id);
637
+ };
638
+ if (reducedMotion) {
639
+ translate.value = landing;
640
+ land();
641
+ return;
642
+ }
643
+ translate.value = withSpring(landing, LAND, land);
644
+ });
645
+ if (activation === 'longPress') return gesture.activateAfterLongPress(longPressDelay);
646
+
647
+ /*
648
+ * On a handle the drag starts on movement rather than on a hold, but not
649
+ * on the first pixel: a list inside a scroller has to be able to scroll
650
+ * from a finger that happened to land on a grip.
651
+ */
652
+ return gesture.activeOffsetY([-HANDLE_SLOP, HANDLE_SLOP]);
653
+ }, [activation, activeId, locked, dragSeq, fingerY, gap, haptics, heights, id, lift, longPressDelay, notifyBegin, notifySettled, notifyTick, order, reducedMotion, rendered, scrollAtStart, scrollOffset, translate]);
654
+ const style = useAnimatedStyle(() => {
655
+ /*
656
+ * Read rather than animated here. The springs live on `translate` and
657
+ * `lift`, which the gesture drives; starting one from inside the style
658
+ * instead would re-enter it on every frame of the drag, because this
659
+ * worklet re-runs every time the row it is carrying moves.
660
+ */
661
+ if (activeId.value === id) {
662
+ return {
663
+ transform: [{
664
+ translateY: translate.value
665
+ }, {
666
+ scale: 1 + lift.value * (LIFT_SCALE - 1)
667
+ }]
668
+ };
669
+ }
670
+ const map = heights.value;
671
+ const offset = slotOffset(order.value, id, map, gap) - slotOffset(rendered.value, id, map, gap);
672
+
673
+ /*
674
+ * Only animated while a drag is in flight, and the difference is the whole
675
+ * end of the drop. When the caller applies the reorder the rows move in
676
+ * the tree and every offset falls to zero on the same commit — springing
677
+ * to it would send the row that was just dropped sliding back across the
678
+ * distance it had travelled, in a slot it was already sitting in.
679
+ */
680
+ const settling = activeId.value === null;
681
+ return {
682
+ transform: [{
683
+ translateY: settling || reducedMotion ? offset : withSpring(offset, DISPLACE)
684
+ }, {
685
+ scale: 1
686
+ }]
687
+ };
688
+ });
689
+ const onLayout = useCallback(event => measured(id, event.nativeEvent.layout.height), [id, measured]);
690
+ const itemContext = useMemo(() => ({
691
+ id,
692
+ index,
693
+ isActive,
694
+ disabled: locked,
695
+ handleGesture: activation === 'handle' ? pan : null
696
+ }), [id, index, isActive, locked, activation, pan]);
697
+
698
+ /**
699
+ * A drag is invisible to a screen reader — there is nothing to announce and
700
+ * no way to discover it from the row. Moving by whole slots is published as
701
+ * an accessibility action instead, which is the only path to reordering for
702
+ * someone who is not dragging anything.
703
+ */
704
+ const a11y = locked ? undefined : [{
705
+ name: 'moveUp',
706
+ label: 'Move up'
707
+ }, {
708
+ name: 'moveDown',
709
+ label: 'Move down'
710
+ }];
711
+ const row = /*#__PURE__*/_jsx(Animated.View, {
712
+ onLayout: onLayout,
713
+ accessibilityActions: a11y,
714
+ onAccessibilityAction: event => {
715
+ if (event.nativeEvent.actionName === 'moveUp') step(id, -1);
716
+ if (event.nativeEvent.actionName === 'moveDown') step(id, 1);
717
+ },
718
+ style: style
719
+ /*
720
+ * A carried row is drawn over the ones it is passing, so it has to have
721
+ * a surface of its own — the row itself is only a box around whatever
722
+ * the caller put inside it, and a good deal of what people put there
723
+ * (an outlined `Item`, a bare `View`) has no background at all. Without
724
+ * this the lifted row is see-through and the list can be read straight
725
+ * through the middle of it.
726
+ *
727
+ * After `className` so the surface holds whatever else the row is
728
+ * wearing, and before `activeClassName`, which is the way out.
729
+ */,
730
+ className: cn('w-full', className, isActive && 'z-10 rounded-xl bg-card shadow-lg', isActive && activeClassName),
731
+ ...props,
732
+ children: children
733
+ });
734
+ return /*#__PURE__*/_jsx(SortableItemContext.Provider, {
735
+ value: itemContext,
736
+ children: activation === 'longPress' ? /*#__PURE__*/_jsx(GestureDetector, {
737
+ gesture: pan,
738
+ children: row
739
+ }) : row
740
+ });
741
+ }
742
+ SortableItem.displayName = 'Sortable.Item';
743
+
744
+ /* -------------------------------------------------------------------------- */
745
+ /* Handle */
746
+ /* -------------------------------------------------------------------------- */
747
+
748
+ /**
749
+ * The part of a row that lifts it.
750
+ *
751
+ * A grip exists so the rest of the row does not have to be given up to the
752
+ * drag: a row with a button, a checkbox or a link on it needs those to stay
753
+ * pressable, and a long press on the whole row takes all of them. It is sized
754
+ * to be hit rather than to be seen, which is why the box around the glyph is
755
+ * larger than the glyph.
756
+ *
757
+ * Inert in a list that lifts on a long press — the whole row already carries
758
+ * the gesture there, and a second one inside it would fight the first.
759
+ */
760
+ function SortableHandle({
761
+ className,
762
+ children,
763
+ accessibilityLabel = 'Drag to reorder',
764
+ ...props
765
+ }) {
766
+ const {
767
+ activation,
768
+ disabled: rootDisabled
769
+ } = useSortableRoot('Sortable.Handle');
770
+ const item = useContext(SortableItemContext);
771
+
772
+ /*
773
+ * The grip is furniture rather than content, so it takes the muted
774
+ * foreground — read from the theme rather than written down, because a hex
775
+ * here stops being right the moment the theme inverts.
776
+ */
777
+ const muted = useCSSVariable('--color-muted-foreground');
778
+ const tint = typeof muted === 'string' ? muted : undefined;
779
+ const glyph = /*#__PURE__*/_jsx(View, {
780
+ accessible: true,
781
+ accessibilityRole: "adjustable",
782
+ accessibilityLabel: accessibilityLabel,
783
+ accessibilityState: {
784
+ disabled: rootDisabled || item?.disabled
785
+ },
786
+ className: cn('items-center justify-center px-2 py-1.5', (rootDisabled || item?.disabled) && 'opacity-40', className),
787
+ ...props,
788
+ children: /*#__PURE__*/_jsx(IconColorProvider, {
789
+ color: tint,
790
+ children: children ?? /*#__PURE__*/_jsx(GripVerticalIcon, {
791
+ size: 18
792
+ })
793
+ })
794
+ });
795
+ if (activation !== 'handle' || !item?.handleGesture) return glyph;
796
+ return /*#__PURE__*/_jsx(GestureDetector, {
797
+ gesture: item.handleGesture,
798
+ children: glyph
799
+ });
800
+ }
801
+ SortableHandle.displayName = 'Sortable.Handle';
802
+ export const Sortable = Object.assign(SortableRoot, {
803
+ Item: SortableItem,
804
+ Handle: SortableHandle
805
+ });
806
+ //# sourceMappingURL=index.js.map