@particle-academy/react-fancy 5.63.0 → 5.64.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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [5.64.0] - 2026-10-26
15
+
16
+ ### Added
17
+
18
+ - **`ReorderableGrid`: cells reordered in place, row-major, by pointer or
19
+ keyboard.** Requested by MOIC for a field-order designer. A deal's details
20
+ are a 2-to-4-column grid of label/value cells, and the designer has to look
21
+ like that grid. Until now it was faked with one `Kanban.Column`'s vertical
22
+ list.
23
+ - **Order is ids.** `value` / `defaultValue` are `string[]`, and `onReorder`
24
+ gets the full new order once per committed move. It is never called for
25
+ the live preview, a cancel or a drop back in place. The order is
26
+ reconciled with `items`: unknown ids are ignored and new items are
27
+ appended, so a stale saved order never hides a cell.
28
+ - **Pointer:** a 4px threshold keeps clicks working. The other cells shift
29
+ live and wrap across rows. Pointer capture is on the root. A release
30
+ outside the grid cancels. Drag handles via `data-reorderable-handle` or
31
+ `state.handleProps`. The nearest scroll container auto-scrolls near its
32
+ edge.
33
+ - **Keyboard:** one tab stop. Space or Enter picks up and drops, arrows move
34
+ in 2-D, and Escape cancels through the `useEscapeKey` layer stack, so
35
+ inside a Modal the pick-up cancels before the Modal closes. An
36
+ `aria-live` region announces each step ("Moved to position 3 of 8, row 1
37
+ column 3"). Every message can be overridden through `labels`.
38
+ - **`locked: true`** pins a cell at its absolute index. Other cells flow
39
+ around it, and nothing can be dropped on it.
40
+ - **`moveItem(ids, from, to, { locked })`** is exported: the same rule with no
41
+ DOM, for agent bridges.
42
+ - Subpath `@particle-academy/react-fancy/reorderable-grid`. Docs:
43
+ `docs/ReorderableGrid.md`. New component only; nothing existing changed.
44
+
14
45
  ## [5.63.0] - 2026-10-26
15
46
 
16
47
  ### Added
package/README.md CHANGED
@@ -165,6 +165,7 @@ npx vite build # Build demo app (verifies imports work)
165
165
  | Editor | Toolbar chrome wrapper for contentEditable, with a Source toggle to edit raw HTML/Markdown | [docs](docs/Editor.md) |
166
166
  | CodeView | Lightweight syntax-highlighted source view (HTML highlighted; fills height) | [docs](docs/CodeView.md) |
167
167
  | Kanban | Drag-and-drop board with columns and cards | [docs](docs/Kanban.md) |
168
+ | ReorderableGrid | Row-major grid of cells reordered in place by pointer or keyboard, with pinned cells and a pure `moveItem` for agents | [docs](docs/ReorderableGrid.md) |
168
169
  | ContentRenderer | Markdown/HTML content renderer | [docs](docs/ContentRenderer.md) |
169
170
 
170
171
  ### Media Viewers
@@ -0,0 +1,605 @@
1
+ import { composeRefs } from './chunk-SV5BW3IA.js';
2
+ import { useEscapeKey } from './chunk-YX6FTJ6N.js';
3
+ import { useControllableState } from './chunk-EDYAXGUM.js';
4
+ import { cn } from './chunk-MMW2JGKZ.js';
5
+ import { forwardRef, useMemo, useCallback, useState, useRef, useId, useEffect, useLayoutEffect } from 'react';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
+
8
+ // src/components/ReorderableGrid/move-item.ts
9
+ function lockPredicate(locked) {
10
+ if (!locked) return null;
11
+ if (typeof locked === "function") return locked;
12
+ const set = new Set(locked);
13
+ return set.size ? (id) => set.has(id) : null;
14
+ }
15
+ function moveItem(ids, fromIndex, toIndex, options = {}) {
16
+ const n = ids.length;
17
+ for (const [name, i] of [
18
+ ["fromIndex", fromIndex],
19
+ ["toIndex", toIndex]
20
+ ]) {
21
+ if (!Number.isInteger(i) || i < 0 || i >= n) {
22
+ throw new RangeError(`moveItem: ${name} ${i} is outside 0..${n - 1}`);
23
+ }
24
+ }
25
+ const out = ids.slice();
26
+ if (fromIndex === toIndex) return out;
27
+ const isLocked = lockPredicate(options.locked);
28
+ if (!isLocked) {
29
+ const [moved2] = out.splice(fromIndex, 1);
30
+ out.splice(toIndex, 0, moved2);
31
+ return out;
32
+ }
33
+ if (isLocked(ids[fromIndex]) || isLocked(ids[toIndex])) return out;
34
+ const slots = [];
35
+ for (let i = 0; i < n; i++) if (!isLocked(ids[i])) slots.push(i);
36
+ const free = slots.map((i) => ids[i]);
37
+ const [moved] = free.splice(slots.indexOf(fromIndex), 1);
38
+ free.splice(slots.indexOf(toIndex), 0, moved);
39
+ slots.forEach((slot, k) => {
40
+ out[slot] = free[k];
41
+ });
42
+ return out;
43
+ }
44
+ var COLUMN_CLASSES = [
45
+ "grid-cols-1",
46
+ "grid-cols-2",
47
+ "grid-cols-3",
48
+ "grid-cols-4",
49
+ "grid-cols-5",
50
+ "grid-cols-6",
51
+ "grid-cols-7",
52
+ "grid-cols-8",
53
+ "grid-cols-9",
54
+ "grid-cols-10",
55
+ "grid-cols-11",
56
+ "grid-cols-12"
57
+ ];
58
+ var GAP_CLASSES = { none: "gap-0", sm: "gap-3", md: "gap-6", lg: "gap-10" };
59
+ var FOCUS_RING = "ring-[color:var(--fancy-focus,var(--color-blue-500))] dark:ring-[color:var(--fancy-focus-dark,var(--color-blue-400))]";
60
+ var CELL_BASE = "relative min-w-0 select-none touch-none outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--fancy-focus,var(--color-blue-500))] dark:focus-visible:ring-[color:var(--fancy-focus-dark,var(--color-blue-400))]";
61
+ var CELL_STYLED = "rounded-lg border p-3 text-sm border-[color:var(--fancy-border,var(--color-zinc-200))] dark:border-[color:var(--fancy-border-dark,var(--color-zinc-700))] bg-[color:var(--fancy-surface,var(--color-white))] dark:bg-[color:var(--fancy-surface-dark,var(--color-zinc-900))] text-[color:var(--fancy-text,var(--color-zinc-900))] dark:text-[color:var(--fancy-text-dark,var(--color-zinc-100))]";
62
+ var CELL_LOCKED_STYLED = "border-dashed bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]";
63
+ var DRAG_THRESHOLD = 4;
64
+ var EDGE = 48;
65
+ var MAX_SCROLL_STEP = 18;
66
+ var SHIFT_MS = 150;
67
+ var INTERACTIVE = "input, textarea, select, button, a[href], [contenteditable=''], [contenteditable='true']";
68
+ var DEFAULT_LABELS = {
69
+ instructions: "Press Space or Enter to pick up. Use the arrow keys to move, Space or Enter to drop, Escape to cancel.",
70
+ roleDescription: "reorderable item",
71
+ pickedUp: (c) => `Picked up ${c.label} at position ${c.position} of ${c.total}, row ${c.row} column ${c.column}. Arrow keys move it, Space or Enter drops it, Escape cancels`,
72
+ moved: (c) => `Moved to position ${c.position} of ${c.total}, row ${c.row} column ${c.column}`,
73
+ dropped: (c) => `Dropped ${c.label} at position ${c.position} of ${c.total}, row ${c.row} column ${c.column}`,
74
+ cancelled: (c) => `Cancelled. ${c.label} returned to position ${c.position} of ${c.total}, row ${c.row} column ${c.column}`,
75
+ locked: (c) => `${c.label} is locked and cannot be moved`
76
+ };
77
+ function resolveOrder(order, items) {
78
+ const ids = items.map((i) => i.id);
79
+ const known = new Set(ids);
80
+ const seen = /* @__PURE__ */ new Set();
81
+ const out = [];
82
+ for (const id of order ?? []) {
83
+ if (known.has(id) && !seen.has(id)) {
84
+ seen.add(id);
85
+ out.push(id);
86
+ }
87
+ }
88
+ for (const id of ids) {
89
+ if (!seen.has(id)) {
90
+ seen.add(id);
91
+ out.push(id);
92
+ }
93
+ }
94
+ return out;
95
+ }
96
+ function sameOrder(a, b) {
97
+ return a.length === b.length && a.every((id, i) => id === b[i]);
98
+ }
99
+ function prefersReducedMotion() {
100
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
101
+ }
102
+ function scrollParent(el) {
103
+ for (let p = el.parentElement; p; p = p.parentElement) {
104
+ const s = getComputedStyle(p);
105
+ if (/(auto|scroll)/.test(s.overflowY + s.overflowX)) {
106
+ if (p.scrollHeight > p.clientHeight || p.scrollWidth > p.clientWidth) return p;
107
+ }
108
+ }
109
+ return document.scrollingElement ?? null;
110
+ }
111
+ function ReorderableGridInner({
112
+ items,
113
+ value,
114
+ defaultValue,
115
+ onReorder,
116
+ renderItem,
117
+ columns = 2,
118
+ gap = "sm",
119
+ disabled = false,
120
+ label,
121
+ getItemLabel,
122
+ labels: labelOverrides,
123
+ unstyled = false,
124
+ cellClassName,
125
+ className,
126
+ ...props
127
+ }, forwardedRef) {
128
+ const cols = Math.min(12, Math.max(1, Math.round(columns) || 1));
129
+ const labels = useMemo(() => ({ ...DEFAULT_LABELS, ...labelOverrides }), [labelOverrides]);
130
+ const [stored, setStored] = useControllableState(value, defaultValue, (next) => {
131
+ if (next) onReorder?.(next);
132
+ });
133
+ const committed = useMemo(() => resolveOrder(stored, items), [stored, items]);
134
+ const byId = useMemo(() => new Map(items.map((i) => [i.id, i])), [items]);
135
+ const lockedIds = useMemo(() => items.filter((i) => i.locked).map((i) => i.id), [items]);
136
+ const isLocked = useCallback((id) => byId.get(id)?.locked === true, [byId]);
137
+ const [session, setSessionState] = useState(null);
138
+ const sessionRef = useRef(null);
139
+ const setSession = useCallback((s) => {
140
+ sessionRef.current = s;
141
+ setSessionState(s);
142
+ }, []);
143
+ const previewOf = useCallback(
144
+ (s) => s.target === null || s.target === s.originIndex ? s.origin : moveItem(s.origin, s.originIndex, s.target, { locked: lockedIds }),
145
+ [lockedIds]
146
+ );
147
+ const shown = useMemo(
148
+ () => session ? resolveOrder(previewOf(session), items) : committed,
149
+ [session, previewOf, items, committed]
150
+ );
151
+ const shownRef = useRef(shown);
152
+ shownRef.current = shown;
153
+ const [announcement, setAnnouncement] = useState("");
154
+ const labelOf = useCallback(
155
+ (id) => {
156
+ const item = byId.get(id);
157
+ if (!item) return id;
158
+ if (getItemLabel) return getItemLabel(item);
159
+ const l = item.label;
160
+ return typeof l === "string" ? l : id;
161
+ },
162
+ [byId, getItemLabel]
163
+ );
164
+ const ctxAt = useCallback(
165
+ (id, index, total2) => ({
166
+ id,
167
+ label: labelOf(id),
168
+ position: index + 1,
169
+ total: total2,
170
+ row: Math.floor(index / cols) + 1,
171
+ column: index % cols + 1
172
+ }),
173
+ [labelOf, cols]
174
+ );
175
+ const rootRef = useRef(null);
176
+ const cellRefs = useRef(/* @__PURE__ */ new Map());
177
+ const cellRef = useCallback(
178
+ (id) => (el) => {
179
+ if (el) cellRefs.current.set(id, el);
180
+ else cellRefs.current.delete(id);
181
+ },
182
+ []
183
+ );
184
+ const instructionsId = useId();
185
+ const [activeId, setActiveId] = useState(null);
186
+ const tabStop = activeId !== null && shown.includes(activeId) ? activeId : shown[0];
187
+ const refocus = useRef(null);
188
+ const drag = useRef(null);
189
+ const pending = useRef(null);
190
+ const scrollFrame = useRef(null);
191
+ const settle = useCallback((id) => {
192
+ const el = cellRefs.current.get(id);
193
+ if (!el) return;
194
+ if (el.style.transform && !prefersReducedMotion()) {
195
+ el.style.transition = `transform ${SHIFT_MS}ms ease`;
196
+ window.setTimeout(() => {
197
+ if (!el.style.transform) el.style.transition = "";
198
+ }, SHIFT_MS + 50);
199
+ }
200
+ el.style.transform = "";
201
+ }, []);
202
+ const endPointer = useCallback(() => {
203
+ const d = drag.current;
204
+ drag.current = null;
205
+ pending.current = null;
206
+ if (scrollFrame.current !== null) {
207
+ cancelAnimationFrame(scrollFrame.current);
208
+ scrollFrame.current = null;
209
+ }
210
+ if (!d) return;
211
+ const root = rootRef.current;
212
+ if (root?.hasPointerCapture?.(d.pointerId)) root.releasePointerCapture?.(d.pointerId);
213
+ settle(d.id);
214
+ }, [settle]);
215
+ const cancel = useCallback(() => {
216
+ const s = sessionRef.current;
217
+ if (!s) return;
218
+ endPointer();
219
+ setSession(null);
220
+ if (s.mode === "keyboard") refocus.current = s.id;
221
+ setAnnouncement(labels.cancelled(ctxAt(s.id, s.originIndex, s.origin.length)));
222
+ }, [ctxAt, endPointer, labels, setSession]);
223
+ const drop = useCallback(() => {
224
+ const s = sessionRef.current;
225
+ if (!s) return;
226
+ if (s.target === null) {
227
+ cancel();
228
+ return;
229
+ }
230
+ const final = previewOf(s);
231
+ endPointer();
232
+ setSession(null);
233
+ if (s.mode === "keyboard") refocus.current = s.id;
234
+ setAnnouncement(labels.dropped(ctxAt(s.id, s.target, final.length)));
235
+ if (!sameOrder(final, s.origin)) setStored(final);
236
+ }, [cancel, ctxAt, endPointer, labels, previewOf, setSession, setStored]);
237
+ useEscapeKey(cancel, session !== null);
238
+ useEffect(() => {
239
+ if (disabled) cancel();
240
+ }, [disabled, cancel]);
241
+ useEffect(() => () => endPointer(), [endPointer]);
242
+ const begin = useCallback(
243
+ (id, mode) => {
244
+ const origin = shownRef.current;
245
+ const index = origin.indexOf(id);
246
+ if (index < 0) return false;
247
+ if (isLocked(id)) {
248
+ setAnnouncement(labels.locked(ctxAt(id, index, origin.length)));
249
+ return false;
250
+ }
251
+ setSession({ id, mode, origin, originIndex: index, target: index });
252
+ setAnnouncement(labels.pickedUp(ctxAt(id, index, origin.length)));
253
+ return true;
254
+ },
255
+ [ctxAt, isLocked, labels, setSession]
256
+ );
257
+ const retarget = useCallback(
258
+ (target) => {
259
+ const s = sessionRef.current;
260
+ if (!s || target === s.target) return;
261
+ setSession({ ...s, target });
262
+ if (target !== null) setAnnouncement(labels.moved(ctxAt(s.id, target, s.origin.length)));
263
+ },
264
+ [ctxAt, labels, setSession]
265
+ );
266
+ const step = useCallback(
267
+ (order, from, key, skipLocked) => {
268
+ const n = order.length;
269
+ const free = (i) => !skipLocked || !isLocked(order[i]);
270
+ const scan = (start, dir) => {
271
+ for (let i = start; i >= 0 && i < n; i += dir) if (free(i)) return i;
272
+ return null;
273
+ };
274
+ switch (key) {
275
+ case "ArrowRight":
276
+ return scan(from + 1, 1);
277
+ case "ArrowLeft":
278
+ return scan(from - 1, -1);
279
+ case "ArrowDown": {
280
+ let t = from + cols;
281
+ if (t > n - 1) {
282
+ if (Math.floor(from / cols) >= Math.floor((n - 1) / cols)) return null;
283
+ t = n - 1;
284
+ }
285
+ return scan(t, 1);
286
+ }
287
+ case "ArrowUp":
288
+ return from - cols < 0 ? null : scan(from - cols, -1);
289
+ case "Home":
290
+ return scan(0, 1);
291
+ case "End":
292
+ return scan(n - 1, -1);
293
+ default:
294
+ return null;
295
+ }
296
+ },
297
+ [cols, isLocked]
298
+ );
299
+ const onCellKeyDown = (e, id) => {
300
+ if (disabled || e.target !== e.currentTarget) return;
301
+ const s = sessionRef.current;
302
+ if (e.key === " " || e.key === "Enter") {
303
+ e.preventDefault();
304
+ if (s?.mode === "keyboard") drop();
305
+ else if (!s) begin(id, "keyboard");
306
+ return;
307
+ }
308
+ if (!["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp", "Home", "End"].includes(e.key)) return;
309
+ e.preventDefault();
310
+ if (s?.mode === "keyboard") {
311
+ if (s.target === null) return;
312
+ const t2 = step(shownRef.current, s.target, e.key, true);
313
+ if (t2 !== null) retarget(t2);
314
+ return;
315
+ }
316
+ if (s) return;
317
+ const order = shownRef.current;
318
+ const t = step(order, order.indexOf(id), e.key, false);
319
+ if (t === null) return;
320
+ const next = order[t];
321
+ setActiveId(next);
322
+ cellRefs.current.get(next)?.focus();
323
+ };
324
+ const toLocal = (clientX, clientY) => {
325
+ const root = rootRef.current;
326
+ const r = root.getBoundingClientRect();
327
+ return { x: clientX - r.left - root.clientLeft, y: clientY - r.top - root.clientTop, rect: r };
328
+ };
329
+ const hitTest = (clientX, clientY) => {
330
+ const s = sessionRef.current;
331
+ if (!s) return null;
332
+ const { x, y, rect } = toLocal(clientX, clientY);
333
+ if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) return null;
334
+ const order = shownRef.current;
335
+ let best = -1;
336
+ let bestDist = Infinity;
337
+ order.forEach((id, i) => {
338
+ const el = cellRefs.current.get(id);
339
+ if (!el) return;
340
+ const l = el.offsetLeft;
341
+ const t = el.offsetTop;
342
+ const dx = Math.max(l - x, 0, x - (l + el.offsetWidth));
343
+ const dy = Math.max(t - y, 0, y - (t + el.offsetHeight));
344
+ const dist = dx * dx + dy * dy;
345
+ if (dist < bestDist) {
346
+ bestDist = dist;
347
+ best = i;
348
+ }
349
+ });
350
+ if (best < 0) return s.target;
351
+ if (isLocked(order[best])) return s.target ?? s.originIndex;
352
+ return best;
353
+ };
354
+ const applyDragTransform = useCallback(() => {
355
+ const d = drag.current;
356
+ const root = rootRef.current;
357
+ if (!d || !root) return;
358
+ const el = cellRefs.current.get(d.id);
359
+ if (!el) return;
360
+ const r = root.getBoundingClientRect();
361
+ const px = d.clientX - r.left - root.clientLeft;
362
+ const py = d.clientY - r.top - root.clientTop;
363
+ const tx = d.startOffset.x + (px - d.startPos.x) - el.offsetLeft;
364
+ const ty = d.startOffset.y + (py - d.startPos.y) - el.offsetTop;
365
+ el.style.transition = "";
366
+ el.style.transform = `translate3d(${tx}px, ${ty}px, 0)`;
367
+ }, []);
368
+ const track = () => {
369
+ const d = drag.current;
370
+ if (!d) return;
371
+ retarget(hitTest(d.clientX, d.clientY));
372
+ applyDragTransform();
373
+ };
374
+ const autoScroll = () => {
375
+ const d = drag.current;
376
+ if (!d?.scroller || typeof requestAnimationFrame !== "function") return;
377
+ const sc = d.scroller;
378
+ const isDoc = sc === document.scrollingElement;
379
+ const box = isDoc ? { top: 0, left: 0, bottom: window.innerHeight, right: window.innerWidth } : sc.getBoundingClientRect();
380
+ const speed = (dist) => dist < EDGE ? Math.ceil((EDGE - dist) / EDGE * MAX_SCROLL_STEP) : 0;
381
+ let vy = speed(d.clientY - box.top) ? -speed(d.clientY - box.top) : speed(box.bottom - d.clientY);
382
+ let vx = speed(d.clientX - box.left) ? -speed(d.clientX - box.left) : speed(box.right - d.clientX);
383
+ if (vy < 0 && sc.scrollTop <= 0 || vy > 0 && sc.scrollTop + sc.clientHeight >= sc.scrollHeight) vy = 0;
384
+ if (vx < 0 && sc.scrollLeft <= 0 || vx > 0 && sc.scrollLeft + sc.clientWidth >= sc.scrollWidth) vx = 0;
385
+ if (!vx && !vy) {
386
+ scrollFrame.current = null;
387
+ return;
388
+ }
389
+ if (scrollFrame.current !== null) return;
390
+ scrollFrame.current = requestAnimationFrame(() => {
391
+ scrollFrame.current = null;
392
+ if (!drag.current) return;
393
+ sc.scrollTop += vy;
394
+ sc.scrollLeft += vx;
395
+ track();
396
+ autoScroll();
397
+ });
398
+ };
399
+ const onCellPointerDown = (e, id) => {
400
+ if (disabled || e.button !== 0 || sessionRef.current || isLocked(id)) return;
401
+ const cellEl = e.currentTarget;
402
+ const target = e.target;
403
+ const handle = target.closest("[data-reorderable-handle]");
404
+ if (handle && cellEl.contains(handle)) ; else if (cellEl.querySelector("[data-reorderable-handle]")) {
405
+ return;
406
+ } else {
407
+ const interactive = target.closest(INTERACTIVE);
408
+ if (interactive && interactive !== cellEl && cellEl.contains(interactive)) return;
409
+ }
410
+ pending.current = { id, pointerId: e.pointerId, clientX: e.clientX, clientY: e.clientY };
411
+ };
412
+ const onRootPointerMove = (e) => {
413
+ const p = pending.current;
414
+ if (p && p.pointerId === e.pointerId && !drag.current) {
415
+ if (Math.hypot(e.clientX - p.clientX, e.clientY - p.clientY) < DRAG_THRESHOLD) return;
416
+ const root = rootRef.current;
417
+ const el = cellRefs.current.get(p.id);
418
+ pending.current = null;
419
+ if (!root || !el || !begin(p.id, "pointer")) return;
420
+ root.setPointerCapture?.(e.pointerId);
421
+ window.getSelection?.()?.removeAllRanges();
422
+ const start = toLocal(p.clientX, p.clientY);
423
+ drag.current = {
424
+ id: p.id,
425
+ pointerId: e.pointerId,
426
+ startOffset: { x: el.offsetLeft, y: el.offsetTop },
427
+ startPos: { x: start.x, y: start.y },
428
+ clientX: e.clientX,
429
+ clientY: e.clientY,
430
+ scroller: scrollParent(root)
431
+ };
432
+ }
433
+ const d = drag.current;
434
+ if (!d || d.pointerId !== e.pointerId) return;
435
+ d.clientX = e.clientX;
436
+ d.clientY = e.clientY;
437
+ track();
438
+ autoScroll();
439
+ };
440
+ const onRootPointerUp = (e) => {
441
+ if (pending.current?.pointerId === e.pointerId) {
442
+ pending.current = null;
443
+ return;
444
+ }
445
+ const d = drag.current;
446
+ if (!d || d.pointerId !== e.pointerId) return;
447
+ d.clientX = e.clientX;
448
+ d.clientY = e.clientY;
449
+ const target = hitTest(e.clientX, e.clientY);
450
+ const s = sessionRef.current;
451
+ if (s && target !== s.target) {
452
+ sessionRef.current = { ...s, target };
453
+ }
454
+ drop();
455
+ };
456
+ const onRootPointerCancel = (e) => {
457
+ if (pending.current?.pointerId === e.pointerId) pending.current = null;
458
+ if (drag.current?.pointerId === e.pointerId) cancel();
459
+ };
460
+ const offsets = useRef(/* @__PURE__ */ new Map());
461
+ const shiftTimers = useRef(/* @__PURE__ */ new Map());
462
+ const shownKey = shown.join("\0");
463
+ useLayoutEffect(() => {
464
+ const prev = offsets.current;
465
+ const next = /* @__PURE__ */ new Map();
466
+ for (const [id, el] of cellRefs.current) next.set(id, { x: el.offsetLeft, y: el.offsetTop });
467
+ offsets.current = next;
468
+ if (prev.size && !prefersReducedMotion()) {
469
+ for (const [id, el] of cellRefs.current) {
470
+ if (id === drag.current?.id) continue;
471
+ const a = prev.get(id);
472
+ const b = next.get(id);
473
+ if (!a || !b || a.x === b.x && a.y === b.y) continue;
474
+ el.style.transition = "none";
475
+ el.style.transform = `translate3d(${a.x - b.x}px, ${a.y - b.y}px, 0)`;
476
+ void el.offsetWidth;
477
+ el.style.transition = `transform ${SHIFT_MS}ms ease`;
478
+ el.style.transform = "";
479
+ const old = shiftTimers.current.get(id);
480
+ if (old !== void 0) window.clearTimeout(old);
481
+ shiftTimers.current.set(
482
+ id,
483
+ window.setTimeout(() => {
484
+ shiftTimers.current.delete(id);
485
+ if (!el.style.transform) el.style.transition = "";
486
+ }, SHIFT_MS + 50)
487
+ );
488
+ }
489
+ }
490
+ applyDragTransform();
491
+ const s = sessionRef.current;
492
+ const keep = s?.mode === "keyboard" ? s.id : refocus.current;
493
+ refocus.current = null;
494
+ if (keep) {
495
+ const el = cellRefs.current.get(keep);
496
+ if (el && document.activeElement !== el) el.focus();
497
+ }
498
+ }, [shownKey, session, applyDragTransform]);
499
+ useEffect(() => {
500
+ const timers = shiftTimers.current;
501
+ return () => {
502
+ for (const t of timers.values()) window.clearTimeout(t);
503
+ };
504
+ }, []);
505
+ const state = session ? session.mode === "pointer" ? "dragging" : "picked-up" : "idle";
506
+ const dropIndex = session && session.target !== null ? session.target : void 0;
507
+ const total = shown.length;
508
+ return /* @__PURE__ */ jsxs(
509
+ "div",
510
+ {
511
+ ...props,
512
+ ref: composeRefs(rootRef, forwardedRef),
513
+ "data-react-fancy-reorderable-grid": "",
514
+ "data-columns": cols,
515
+ "data-state": state,
516
+ "data-drop-index": dropIndex,
517
+ "data-disabled": disabled ? "" : void 0,
518
+ role: "group",
519
+ "aria-label": label,
520
+ "aria-disabled": disabled || void 0,
521
+ onPointerMove: onRootPointerMove,
522
+ onPointerUp: onRootPointerUp,
523
+ onPointerCancel: onRootPointerCancel,
524
+ onLostPointerCapture: (e) => {
525
+ if (drag.current?.pointerId === e.pointerId) cancel();
526
+ },
527
+ className: cn(
528
+ "relative grid",
529
+ COLUMN_CLASSES[cols - 1],
530
+ GAP_CLASSES[gap],
531
+ session?.mode === "pointer" && "cursor-grabbing select-none",
532
+ disabled && "opacity-60",
533
+ className
534
+ ),
535
+ children: [
536
+ shown.map((id, index) => {
537
+ const item = byId.get(id);
538
+ if (!item) return null;
539
+ const locked = item.locked === true;
540
+ const active = session?.id === id;
541
+ const dragging = active && session?.mode === "pointer";
542
+ const pickedUp = active && session?.mode === "keyboard";
543
+ const dropTarget = active && dropIndex === index;
544
+ const outside = active && session?.target === null;
545
+ const itemState = {
546
+ index,
547
+ position: index + 1,
548
+ row: Math.floor(index / cols) + 1,
549
+ column: index % cols + 1,
550
+ total,
551
+ dragging,
552
+ pickedUp,
553
+ dropTarget,
554
+ locked,
555
+ disabled,
556
+ handleProps: { "data-reorderable-handle": "" }
557
+ };
558
+ return /* @__PURE__ */ jsx(
559
+ "div",
560
+ {
561
+ ref: cellRef(id),
562
+ "data-react-fancy-reorderable-grid-item": "",
563
+ "data-reorderable-id": id,
564
+ "data-index": index,
565
+ "data-state": dragging ? "dragging" : pickedUp ? "picked-up" : "idle",
566
+ "data-locked": locked ? "true" : void 0,
567
+ "data-drop-target": dropTarget ? "" : void 0,
568
+ role: "button",
569
+ "aria-roledescription": labels.roleDescription,
570
+ "aria-describedby": instructionsId,
571
+ "aria-pressed": active,
572
+ "aria-disabled": disabled || locked || void 0,
573
+ tabIndex: disabled ? -1 : id === tabStop ? 0 : -1,
574
+ onFocus: (e) => {
575
+ if (e.target === e.currentTarget) setActiveId(id);
576
+ },
577
+ onKeyDown: (e) => onCellKeyDown(e, id),
578
+ onPointerDown: (e) => onCellPointerDown(e, id),
579
+ className: cn(
580
+ CELL_BASE,
581
+ !unstyled && CELL_STYLED,
582
+ !unstyled && locked && CELL_LOCKED_STYLED,
583
+ locked || disabled ? "cursor-default" : "cursor-grab",
584
+ dragging && cn("z-10 cursor-grabbing shadow-lg ring-2", FOCUS_RING),
585
+ pickedUp && cn("z-10 shadow-md ring-2", FOCUS_RING),
586
+ outside && "opacity-60",
587
+ cellClassName
588
+ ),
589
+ children: renderItem ? renderItem(item, itemState) : labelOf(id)
590
+ },
591
+ id
592
+ );
593
+ }),
594
+ /* @__PURE__ */ jsx("div", { id: instructionsId, className: "sr-only", children: labels.instructions }),
595
+ /* @__PURE__ */ jsx("div", { "data-react-fancy-reorderable-grid-live": "", "aria-live": "assertive", "aria-atomic": "true", className: "sr-only", children: announcement })
596
+ ]
597
+ }
598
+ );
599
+ }
600
+ var ReorderableGrid = forwardRef(ReorderableGridInner);
601
+ ReorderableGrid.displayName = "ReorderableGrid";
602
+
603
+ export { ReorderableGrid, moveItem };
604
+ //# sourceMappingURL=chunk-DTOIO42M.js.map
605
+ //# sourceMappingURL=chunk-DTOIO42M.js.map