@waveso/ui 0.15.1 → 0.17.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 (87) hide show
  1. package/README.md +13 -12
  2. package/dist/accordion.js +2 -2
  3. package/dist/action-bar.js +4 -4
  4. package/dist/alert-dialog.js +5 -5
  5. package/dist/alert.d.ts +8 -1
  6. package/dist/alert.js +13 -4
  7. package/dist/animate.d.ts +1 -1
  8. package/dist/animate.js +2 -9
  9. package/dist/autocomplete.js +3 -3
  10. package/dist/avatar.d.ts +1 -1
  11. package/dist/avatar.js +4 -4
  12. package/dist/badge.d.ts +1 -1
  13. package/dist/badge.js +6 -6
  14. package/dist/button-group.d.ts +4 -2
  15. package/dist/button-group.js +5 -4
  16. package/dist/button.d.ts +1 -1
  17. package/dist/button.js +6 -6
  18. package/dist/card.d.ts +11 -4
  19. package/dist/card.js +27 -14
  20. package/dist/checkbox.js +2 -2
  21. package/dist/combobox.js +4 -4
  22. package/dist/context-menu.d.ts +2 -1
  23. package/dist/context-menu.js +14 -14
  24. package/dist/dialog.js +5 -5
  25. package/dist/drawer.js +1 -1
  26. package/dist/encrypted-text.d.ts +15 -1
  27. package/dist/encrypted-text.js +205 -67
  28. package/dist/field.d.ts +21 -3
  29. package/dist/field.js +36 -8
  30. package/dist/fieldset.d.ts +14 -0
  31. package/dist/fieldset.js +26 -0
  32. package/dist/form.d.ts +14 -14
  33. package/dist/form.js +7 -4
  34. package/dist/glyph-matrix.d.ts +25 -0
  35. package/dist/glyph-matrix.js +266 -0
  36. package/dist/gradient-reveal-text.d.ts +3 -3
  37. package/dist/gradient-reveal-text.js +8 -8
  38. package/dist/input-group.js +1 -1
  39. package/dist/input-otp.d.ts +3 -3
  40. package/dist/input-otp.js +2 -2
  41. package/dist/input.js +1 -1
  42. package/dist/item.d.ts +1 -1
  43. package/dist/item.js +17 -11
  44. package/dist/kbd.d.ts +1 -1
  45. package/dist/kbd.js +2 -2
  46. package/dist/label.js +1 -1
  47. package/dist/lib/merge-refs.d.ts +9 -0
  48. package/dist/lib/merge-refs.js +16 -0
  49. package/dist/lib/utils.js +7 -0
  50. package/dist/masonry.d.ts +1 -1
  51. package/dist/masonry.js +4 -2
  52. package/dist/menu.d.ts +2 -1
  53. package/dist/menu.js +7 -7
  54. package/dist/menubar.js +1 -1
  55. package/dist/pagination.js +1 -1
  56. package/dist/popover.js +1 -1
  57. package/dist/preview-card.js +1 -1
  58. package/dist/progress.js +2 -2
  59. package/dist/radio-group.js +1 -1
  60. package/dist/radio.js +2 -2
  61. package/dist/resizable.d.ts +43 -0
  62. package/dist/resizable.js +53 -0
  63. package/dist/scroll-area.d.ts +13 -2
  64. package/dist/scroll-area.js +26 -7
  65. package/dist/select.js +4 -4
  66. package/dist/separator.js +1 -1
  67. package/dist/sidebar.js +12 -12
  68. package/dist/slider.js +3 -3
  69. package/dist/spinner.d.ts +7 -0
  70. package/dist/spinner.js +24 -6
  71. package/dist/styles.css +581 -194
  72. package/dist/swipe-deck.d.ts +98 -0
  73. package/dist/swipe-deck.js +931 -0
  74. package/dist/switch.js +6 -6
  75. package/dist/table.d.ts +7 -1
  76. package/dist/table.js +40 -9
  77. package/dist/tabs.js +3 -3
  78. package/dist/textarea.js +1 -1
  79. package/dist/toast.js +4 -4
  80. package/dist/toggle-group.js +3 -3
  81. package/dist/toggle.d.ts +1 -1
  82. package/dist/toggle.js +11 -6
  83. package/dist/tooltip.d.ts +3 -1
  84. package/dist/tooltip.js +10 -6
  85. package/dist/video-text.d.ts +25 -0
  86. package/dist/video-text.js +147 -0
  87. package/package.json +6 -1
@@ -0,0 +1,931 @@
1
+ "use client";
2
+ import { cn } from "./lib/utils.js";
3
+ import { Button } from "./button.js";
4
+ import { mergeRefs } from "./lib/merge-refs.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import * as React from "react";
7
+ import { animate, useMotionValue, useReducedMotion } from "motion/react";
8
+ //#region src/swipe-deck.tsx
9
+ const DEFAULT_DIRECTIONS = ["left", "right"];
10
+ const KEY_DIRECTIONS = {
11
+ ArrowLeft: "left",
12
+ ArrowRight: "right",
13
+ ArrowUp: "up",
14
+ ArrowDown: "down"
15
+ };
16
+ /** How far (px) a press moves before it becomes a drag, so taps stay clicks. */
17
+ const DRAG_SLOP = 6;
18
+ /** A card leaves when it comes to rest past this share of its width (or height). */
19
+ const LEAVE_SHARE = .25;
20
+ /** Only pointer samples this recent (ms) set the release speed. */
21
+ const VELOCITY_WINDOW = 100;
22
+ /**
23
+ * How much a throw counts: its release speed adds this many seconds' worth of
24
+ * travel to where the card is, to decide whether it leaves and which way.
25
+ */
26
+ const THROW_TIME = .25;
27
+ /** A leaving card's exit point is this far (px) past the stack's edge, shadow and all. */
28
+ const CLEARANCE = 16;
29
+ const RETURN_SPRING = {
30
+ type: "spring",
31
+ stiffness: 400,
32
+ damping: 32
33
+ };
34
+ /**
35
+ * Carries a leaving card to its exit point. A slow card slides out on it; a
36
+ * thrown one brings its speed and runs out against it, so the harder the
37
+ * throw, the harder the pull back: a limit, not a coast to anywhere.
38
+ */
39
+ const EXIT_SPRING = {
40
+ type: "spring",
41
+ stiffness: 650,
42
+ damping: 42
43
+ };
44
+ /** A leaving card fades once clear of the stack and slowed below this (px/s). */
45
+ const CAUGHT_SPEED = 200;
46
+ /** Moves a card from one place in the pile to the next. */
47
+ const PLACE_SPRING = {
48
+ type: "spring",
49
+ stiffness: 260,
50
+ damping: 24
51
+ };
52
+ /** How far (in places) the cards behind lift toward the front as it's dragged out. */
53
+ const LIFT = .4;
54
+ /** On mount, the cards behind wait this long (s) for the front card to fade in. */
55
+ const ENTER_LEAD = .15;
56
+ /** Then each card behind comes this long (s) after the one before. */
57
+ const ENTER_STAGGER = .06;
58
+ /** And grows in from this size, the zoom recipes' md step (`--scale-md`). */
59
+ const ENTER_SCALE = .9;
60
+ /** Dash and gap (px) of `SwipeDeckEmpty`'s outline. */
61
+ const OUTLINE_DASH = 8;
62
+ const OUTLINE_GAP = 6;
63
+ /** Points drawn per corner of that outline. */
64
+ const OUTLINE_CORNER_STEPS = 24;
65
+ /** Presses on these inside a card stay theirs: a slider's thumb, a text box. */
66
+ const FIELD_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [role=slider]";
67
+ /** Keeps the browser's own panning on the axis the deck doesn't swipe along. */
68
+ function touchAction(directions) {
69
+ const horizontal = directions.some(isHorizontal);
70
+ const vertical = directions.some((direction) => !isHorizontal(direction));
71
+ if (horizontal && vertical) return "touch-none";
72
+ return horizontal ? "touch-pan-y" : "touch-pan-x";
73
+ }
74
+ function isHorizontal(direction) {
75
+ return direction === "left" || direction === "right";
76
+ }
77
+ /** Lets the cards behind settle back from a lift. */
78
+ function releaseLift(lift, isInstant) {
79
+ if (isInstant) lift.jump(0);
80
+ else animate(lift, 0, PLACE_SPRING);
81
+ }
82
+ /**
83
+ * Resolves once an element's running transitions and animations end. jsdom
84
+ * has no Web Animations, and an endless animation of your own would never
85
+ * end, so neither is waited for.
86
+ */
87
+ function transitionsEnd(element) {
88
+ const animations = typeof element.getAnimations === "function" ? element.getAnimations().filter((animation) => animation.effect?.getComputedTiming().endTime !== Infinity) : [];
89
+ return Promise.allSettled(animations.map((animation) => animation.finished));
90
+ }
91
+ /** `value` kept within `min`–`max`, or `fallback` when it isn't a number. */
92
+ function within(value, min, max, fallback) {
93
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
94
+ }
95
+ /**
96
+ * Three steady numbers in [-0.5, 0.5] from a card's value: where it lies in a
97
+ * `pile`, the same on every render and server.
98
+ */
99
+ function scatterOf(value) {
100
+ let hash = 2166136261;
101
+ for (let index = 0; index < value.length; index++) hash = Math.imul(hash ^ value.charCodeAt(index), 16777619);
102
+ const next = () => {
103
+ hash ^= hash << 13;
104
+ hash ^= hash >>> 17;
105
+ hash ^= hash << 5;
106
+ return (hash >>> 0) % 1e3 / 1e3 - .5;
107
+ };
108
+ return [
109
+ next(),
110
+ next(),
111
+ next()
112
+ ];
113
+ }
114
+ /** Layout size, without the transform a moving card carries. */
115
+ function sizeOf(element) {
116
+ return {
117
+ width: element.offsetWidth,
118
+ height: element.offsetHeight
119
+ };
120
+ }
121
+ /** Whether a card moved by (x, y) is off the stack. */
122
+ function isClear(x, y, size) {
123
+ return Math.abs(x) >= size.width || Math.abs(y) >= size.height;
124
+ }
125
+ /**
126
+ * Where a card at (x, y), heading along (dx, dy), first sits `CLEARANCE` past
127
+ * the stack's edge: the nearer of the side edges and the top or bottom edge.
128
+ */
129
+ function exitPoint(x, y, [dx, dy], size) {
130
+ const length = Math.hypot(dx, dy);
131
+ const ux = dx / length;
132
+ const uy = dy / length;
133
+ const steps = [ux === 0 ? Infinity : (Math.sign(ux) * (size.width + CLEARANCE) - x) / ux, uy === 0 ? Infinity : (Math.sign(uy) * (size.height + CLEARANCE) - y) / uy];
134
+ const step = Math.max(0, Math.min(...steps));
135
+ return [x + step * ux, y + step * uy];
136
+ }
137
+ /** How far a movement goes toward a direction; negative when it goes away. */
138
+ function reach(direction, x, y) {
139
+ switch (direction) {
140
+ case "right": return x;
141
+ case "left": return -x;
142
+ case "down": return y;
143
+ case "up": return -y;
144
+ }
145
+ }
146
+ /**
147
+ * The allowed direction a movement leans furthest into, and how far it goes
148
+ * there as a share of the distance that makes a card leave (1 or more leaves).
149
+ */
150
+ function lean(directions, x, y, size) {
151
+ let best = {
152
+ direction: void 0,
153
+ share: 0
154
+ };
155
+ for (const direction of directions) {
156
+ const length = isHorizontal(direction) ? size.width : size.height;
157
+ const share = reach(direction, x, y) / Math.max(length * LEAVE_SHARE, 1);
158
+ if (share > best.share) best = {
159
+ direction,
160
+ share
161
+ };
162
+ }
163
+ return best;
164
+ }
165
+ /** Pointer speed (px/s) over the samples taken just before `time`. */
166
+ function releaseVelocity(samples, time) {
167
+ const recent = samples.filter((sample) => time - sample.time <= VELOCITY_WINDOW);
168
+ const first = recent[0];
169
+ const last = recent.at(-1);
170
+ if (!first || !last || last.time <= first.time) return [0, 0];
171
+ const elapsed = (last.time - first.time) / 1e3;
172
+ return [(last.x - first.x) / elapsed, (last.y - first.y) / elapsed];
173
+ }
174
+ const SwipeDeckContext = React.createContext(null);
175
+ function useSwipeDeckContext(part) {
176
+ const context = React.useContext(SwipeDeckContext);
177
+ if (!context) throw new Error(`${part} must be used within a SwipeDeck.`);
178
+ return context;
179
+ }
180
+ /**
181
+ * A pile of cards people swipe away: drag or throw the front card, press an
182
+ * arrow key while the deck is focused, or use a `SwipeDeckAction`. A let-go
183
+ * card whose position and throw carry it past a quarter of its width leaves:
184
+ * it slides out the way it is heading until clear of the stack, held by a
185
+ * spring that a hard throw runs out against, then fades. Short of that, it
186
+ * springs back. Your list stays the source of truth — `onSwipe` tells you
187
+ * which card left and which way, and you remove it or move it to the back.
188
+ *
189
+ * End the deck with a `SwipeDeckEmpty`: it lies under the last card, so
190
+ * dragging that card shows what comes next. The deck carries `data-empty`
191
+ * once no card is in front (the last one leaving, or none left), for styles
192
+ * of your own.
193
+ *
194
+ * Give the deck an `aria-label`. Screen readers hear the next card after each
195
+ * swipe: its `aria-label` when it has one, otherwise all of its text.
196
+ */
197
+ function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false, layout = "stack", spread = .5, depth = .5, visible = 4, className, style, onKeyDown, children, ref, ...props }) {
198
+ const rootRef = React.useRef(null);
199
+ const mergedRef = React.useMemo(() => mergeRefs(rootRef, ref), [ref]);
200
+ const itemsRef = React.useRef(/* @__PURE__ */ new Map());
201
+ const finishedRef = React.useRef(/* @__PURE__ */ new Map());
202
+ const statusRef = React.useRef(null);
203
+ const emptyRef = React.useRef(null);
204
+ const sizeObserverRef = React.useRef(null);
205
+ const lift = useMotionValue(0);
206
+ const [order, setOrder] = React.useState([]);
207
+ const [leavingList, setLeavingList] = React.useState([]);
208
+ const [isReady, setIsReady] = React.useState(false);
209
+ const onSwipeRef = React.useRef(onSwipe);
210
+ React.useEffect(() => {
211
+ onSwipeRef.current = onSwipe;
212
+ });
213
+ const readOrder = React.useCallback(() => [...itemsRef.current].sort(([, a], [, b]) => a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1).map(([value]) => value), []);
214
+ const syncOrder = React.useCallback(() => {
215
+ const items = itemsRef.current;
216
+ const next = readOrder();
217
+ setOrder((previous) => previous.length === next.length && previous.every((value, index) => value === next[index]) ? previous : next);
218
+ const answered = /* @__PURE__ */ new Set();
219
+ for (const [value, followers] of finishedRef.current) {
220
+ const index = next.indexOf(value);
221
+ const hasMoved = followers.some((follower) => {
222
+ const followerIndex = next.indexOf(follower);
223
+ return followerIndex !== -1 && followerIndex < index;
224
+ });
225
+ if (index === -1 || hasMoved) {
226
+ answered.add(value);
227
+ finishedRef.current.delete(value);
228
+ }
229
+ }
230
+ setLeavingList((previous) => {
231
+ const kept = previous.filter((value) => items.has(value) && !answered.has(value));
232
+ return kept.length === previous.length ? previous : kept;
233
+ });
234
+ }, [readOrder]);
235
+ React.useLayoutEffect(() => {
236
+ syncOrder();
237
+ });
238
+ React.useLayoutEffect(() => {
239
+ setIsReady(true);
240
+ }, []);
241
+ React.useEffect(() => {
242
+ const root = rootRef.current;
243
+ if (!root) return;
244
+ const observer = new MutationObserver(syncOrder);
245
+ observer.observe(root, { childList: true });
246
+ return () => observer.disconnect();
247
+ }, [syncOrder]);
248
+ const sizeObserver = React.useCallback(() => {
249
+ sizeObserverRef.current ??= new ResizeObserver(() => {
250
+ const root = rootRef.current;
251
+ const cards = [...itemsRef.current.values()];
252
+ if (!root || cards.length === 0) return;
253
+ const width = Math.max(...cards.map(({ element }) => element.offsetWidth));
254
+ const height = Math.max(...cards.map(({ element }) => element.offsetHeight));
255
+ root.style.setProperty("--swipe-deck-card-width", `${width}px`);
256
+ root.style.setProperty("--swipe-deck-card-height", `${height}px`);
257
+ });
258
+ return sizeObserverRef.current;
259
+ }, []);
260
+ React.useEffect(() => () => sizeObserverRef.current?.disconnect(), []);
261
+ const register = React.useCallback((value, record) => {
262
+ itemsRef.current.set(value, record);
263
+ sizeObserver().observe(record.element);
264
+ syncOrder();
265
+ return () => {
266
+ if (itemsRef.current.get(value) === record) {
267
+ itemsRef.current.delete(value);
268
+ sizeObserverRef.current?.unobserve(record.element);
269
+ syncOrder();
270
+ }
271
+ };
272
+ }, [sizeObserver, syncOrder]);
273
+ const focusDeck = React.useCallback(() => rootRef.current?.focus(), []);
274
+ const [settledFront, setSettledFront] = React.useState();
275
+ React.useEffect(() => lift.on("change", (latest) => {
276
+ rootRef.current?.style.setProperty("--swipe-deck-lift", String(latest));
277
+ }), [lift]);
278
+ const leave = React.useCallback((value) => {
279
+ setLeavingList((previous) => previous.includes(value) ? previous : [...previous, value]);
280
+ }, []);
281
+ const finish = React.useCallback((details) => {
282
+ const current = readOrder();
283
+ finishedRef.current.set(details.value, current.slice(current.indexOf(details.value) + 1));
284
+ onSwipeRef.current?.(details);
285
+ }, [readOrder]);
286
+ const leaving = React.useMemo(() => new Set(leavingList), [leavingList]);
287
+ const positions = React.useMemo(() => new Map(order.filter((value) => !leaving.has(value)).map((value, index) => [value, index])), [order, leaving]);
288
+ const front = order.find((value) => !leaving.has(value));
289
+ const swipeFront = React.useCallback((direction) => {
290
+ if (disabled || front === void 0 || !directions.includes(direction)) return;
291
+ itemsRef.current.get(front)?.swipe(direction);
292
+ }, [
293
+ directions,
294
+ disabled,
295
+ front
296
+ ]);
297
+ const announcedRef = React.useRef(void 0);
298
+ React.useEffect(() => {
299
+ const previous = announcedRef.current;
300
+ announcedRef.current = front;
301
+ const status = statusRef.current;
302
+ if (previous === void 0 || previous === front || !status) return;
303
+ const element = front === void 0 ? emptyRef.current : itemsRef.current.get(front)?.element;
304
+ status.textContent = element ? element.getAttribute("aria-label") ?? (element.innerText || element.textContent || "") : "";
305
+ }, [front]);
306
+ const handleKeyDown = (event) => {
307
+ onKeyDown?.(event);
308
+ if (event.defaultPrevented || event.target !== event.currentTarget) return;
309
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
310
+ const direction = KEY_DIRECTIONS[event.key];
311
+ if (!direction || disabled || !directions.includes(direction)) return;
312
+ event.preventDefault();
313
+ swipeFront(direction);
314
+ };
315
+ const context = React.useMemo(() => ({
316
+ directions,
317
+ disabled,
318
+ isReady,
319
+ front,
320
+ positions,
321
+ leaving,
322
+ count: order.length,
323
+ lift,
324
+ register,
325
+ leave,
326
+ finish,
327
+ swipeFront,
328
+ isEmpty: isReady && front === void 0,
329
+ emptyRef,
330
+ focusDeck,
331
+ settledFront,
332
+ settle: setSettledFront
333
+ }), [
334
+ directions,
335
+ disabled,
336
+ isReady,
337
+ front,
338
+ positions,
339
+ leaving,
340
+ order.length,
341
+ lift,
342
+ register,
343
+ leave,
344
+ finish,
345
+ swipeFront,
346
+ focusDeck,
347
+ settledFront
348
+ ]);
349
+ return /* @__PURE__ */ jsx(SwipeDeckContext.Provider, {
350
+ value: context,
351
+ children: /* @__PURE__ */ jsxs("div", {
352
+ ref: mergedRef,
353
+ "data-slot": "swipe-deck",
354
+ "data-layout": layout,
355
+ "data-empty": context.isEmpty ? "" : void 0,
356
+ "data-disabled": disabled ? "" : void 0,
357
+ role: "group",
358
+ tabIndex: 0,
359
+ "aria-disabled": disabled || void 0,
360
+ className: cn("group/swipe-deck relative isolate grid outline-hidden", "ring-focus/50 rounded-lg focus-visible:not-has-data-front:ring-3", className),
361
+ style: {
362
+ "--swipe-deck-spread": within(spread, 0, 2, .5),
363
+ "--swipe-deck-depth": within(depth, 0, 1, .5),
364
+ "--swipe-deck-visible": Math.round(within(visible, 1, Infinity, 4)),
365
+ ...style
366
+ },
367
+ onKeyDown: handleKeyDown,
368
+ ...props,
369
+ children: [children, /* @__PURE__ */ jsx("span", {
370
+ ref: statusRef,
371
+ "data-slot": "swipe-deck-status",
372
+ role: "status",
373
+ className: "sr-only"
374
+ })]
375
+ })
376
+ });
377
+ }
378
+ /**
379
+ * One card in the deck. It sits in the deck's first grid cell with every other
380
+ * card, so the deck takes the size of its largest card.
381
+ *
382
+ * Styling hooks: `data-front` on the top card, `data-swiping` while it is
383
+ * dragged, `data-leaving` from the moment it leaves, `data-ending-style` once
384
+ * it is clear of the stack and fading (the `motion-fade-lg` recipe), and
385
+ * `data-swipe-direction` for the way it leans or leaves. CSS variables:
386
+ * `--swipe-deck-index` (its place in the pile, 0 at the front),
387
+ * `--swipe-deck-place` (that place as it springs from one to the next, less
388
+ * the lift while the front card is dragged out, as `--swipe-deck-slot`),
389
+ * `--swipe-deck-movement-x` / `-y` in px, and `--swipe-deck-progress` from 0
390
+ * at rest to 1 where letting go makes the card leave.
391
+ *
392
+ * The card owns its translate, rotate and scale, so its motion recipe can only
393
+ * be a `motion-fade-*` one: a recipe that transitions those would lag every
394
+ * drag.
395
+ */
396
+ function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCapture, onDragStart, ...props }) {
397
+ const deck = useSwipeDeckContext("SwipeDeckItem");
398
+ const { directions, register, leave, finish } = deck;
399
+ const prefersReducedMotion = useReducedMotion();
400
+ const elementRef = React.useRef(null);
401
+ const mergedRef = React.useMemo(() => mergeRefs(elementRef, ref), [ref]);
402
+ const x = useMotionValue(0);
403
+ const y = useMotionValue(0);
404
+ const place = useMotionValue(0);
405
+ const enterScale = useMotionValue(1);
406
+ const drivesLiftRef = React.useRef(false);
407
+ const scatter = React.useMemo(() => scatterOf(value), [value]);
408
+ const leftTowardRef = React.useRef(null);
409
+ const stopWatchingRef = React.useRef(null);
410
+ const dragRef = React.useRef(null);
411
+ const endDragRef = React.useRef(null);
412
+ const sizeRef = React.useRef({
413
+ width: 0,
414
+ height: 0
415
+ });
416
+ const draggedRef = React.useRef(false);
417
+ const [isSwiping, setIsSwiping] = React.useState(false);
418
+ const [isFading, setIsFading] = React.useState(false);
419
+ const [isStarting, setIsStarting] = React.useState(true);
420
+ const [direction, setDirection] = React.useState();
421
+ const position = deck.positions.get(value);
422
+ const isLeaving = deck.leaving.has(value);
423
+ const isFront = deck.front === value;
424
+ const directionsRef = React.useRef(directions);
425
+ const disabledRef = React.useRef(deck.disabled);
426
+ const reducedMotionRef = React.useRef(prefersReducedMotion);
427
+ React.useEffect(() => {
428
+ directionsRef.current = directions;
429
+ disabledRef.current = deck.disabled;
430
+ reducedMotionRef.current = prefersReducedMotion;
431
+ drivesLiftRef.current = isFront && !isLeaving;
432
+ });
433
+ React.useEffect(() => {
434
+ const write = () => {
435
+ const element = elementRef.current;
436
+ if (!element) return;
437
+ const { share } = lean(directionsRef.current, x.get(), y.get(), sizeRef.current);
438
+ element.style.setProperty("--swipe-deck-movement-x", `${x.get()}px`);
439
+ element.style.setProperty("--swipe-deck-movement-y", `${y.get()}px`);
440
+ element.style.setProperty("--swipe-deck-progress", String(Math.min(share, 1)));
441
+ if (drivesLiftRef.current) deck.lift.set(Math.min(share, 1) * LIFT);
442
+ };
443
+ const stopX = x.on("change", write);
444
+ const stopY = y.on("change", write);
445
+ const stopPlace = place.on("change", (latest) => {
446
+ elementRef.current?.style.setProperty("--swipe-deck-place", String(latest));
447
+ });
448
+ const stopEnterScale = enterScale.on("change", (latest) => {
449
+ elementRef.current?.style.setProperty("--swipe-deck-enter-scale", String(latest));
450
+ });
451
+ return () => {
452
+ stopX();
453
+ stopY();
454
+ stopPlace();
455
+ stopEnterScale();
456
+ if (dragRef.current) releaseLift(deck.lift, reducedMotionRef.current);
457
+ endDragRef.current?.();
458
+ stopWatchingRef.current?.();
459
+ x.stop();
460
+ y.stop();
461
+ place.stop();
462
+ enterScale.stop();
463
+ };
464
+ }, [
465
+ deck.lift,
466
+ enterScale,
467
+ place,
468
+ x,
469
+ y
470
+ ]);
471
+ const snapBack = React.useCallback(([vx, vy] = [0, 0]) => {
472
+ if (prefersReducedMotion) {
473
+ x.set(0);
474
+ y.set(0);
475
+ return;
476
+ }
477
+ animate(x, 0, {
478
+ ...RETURN_SPRING,
479
+ velocity: vx
480
+ });
481
+ animate(y, 0, {
482
+ ...RETURN_SPRING,
483
+ velocity: vy
484
+ });
485
+ }, [
486
+ prefersReducedMotion,
487
+ x,
488
+ y
489
+ ]);
490
+ const fling = React.useCallback((to, [vx, vy] = [0, 0]) => {
491
+ endDragRef.current?.();
492
+ setIsSwiping(false);
493
+ leftTowardRef.current = to;
494
+ leave(value);
495
+ setDirection(to);
496
+ drivesLiftRef.current = false;
497
+ releaseLift(deck.lift, prefersReducedMotion);
498
+ setIsStarting(false);
499
+ if (prefersReducedMotion) {
500
+ setIsFading(true);
501
+ return;
502
+ }
503
+ const size = sizeRef.current;
504
+ const aheadX = x.get() + vx * THROW_TIME;
505
+ const aheadY = y.get() + vy * THROW_TIME;
506
+ const sign = to === "right" || to === "down" ? 1 : -1;
507
+ const heading = Math.hypot(aheadX, aheadY) >= 1 ? [aheadX, aheadY] : isHorizontal(to) ? [sign, 0] : [0, sign];
508
+ const [targetX, targetY] = exitPoint(x.get(), y.get(), heading, size);
509
+ const headingLength = Math.hypot(...heading);
510
+ const [ux, uy] = [heading[0] / headingLength, heading[1] / headingLength];
511
+ const fade = () => {
512
+ stopWatchingRef.current?.();
513
+ stopWatchingRef.current = null;
514
+ setIsFading(true);
515
+ };
516
+ const check = () => {
517
+ const outward = x.getVelocity() * ux + y.getVelocity() * uy;
518
+ if (isClear(x.get(), y.get(), size) && outward <= CAUGHT_SPEED) fade();
519
+ };
520
+ stopWatchingRef.current?.();
521
+ const stopX = x.on("change", check);
522
+ const stopY = y.on("change", check);
523
+ stopWatchingRef.current = () => {
524
+ stopX();
525
+ stopY();
526
+ };
527
+ const isMainX = Math.abs(targetX - x.get()) >= Math.abs(targetY - y.get());
528
+ animate(x, targetX, {
529
+ ...EXIT_SPRING,
530
+ velocity: vx,
531
+ onComplete: isMainX ? fade : void 0
532
+ });
533
+ animate(y, targetY, {
534
+ ...EXIT_SPRING,
535
+ velocity: vy,
536
+ onComplete: isMainX ? void 0 : fade
537
+ });
538
+ }, [
539
+ deck.lift,
540
+ leave,
541
+ prefersReducedMotion,
542
+ value,
543
+ x,
544
+ y
545
+ ]);
546
+ React.useLayoutEffect(() => {
547
+ const element = elementRef.current;
548
+ if (!element) return;
549
+ return register(value, {
550
+ element,
551
+ swipe: (to) => {
552
+ sizeRef.current = sizeOf(element);
553
+ fling(to);
554
+ }
555
+ });
556
+ }, [
557
+ fling,
558
+ register,
559
+ value
560
+ ]);
561
+ React.useEffect(() => {
562
+ const element = elementRef.current;
563
+ const to = leftTowardRef.current;
564
+ if (!isFading || !element || !to) return;
565
+ let isCancelled = false;
566
+ const frame = requestAnimationFrame(() => {
567
+ transitionsEnd(element).then(() => {
568
+ if (isCancelled) return;
569
+ x.stop();
570
+ y.stop();
571
+ finish({
572
+ value,
573
+ direction: to
574
+ });
575
+ });
576
+ });
577
+ return () => {
578
+ isCancelled = true;
579
+ cancelAnimationFrame(frame);
580
+ };
581
+ }, [
582
+ finish,
583
+ isFading,
584
+ value,
585
+ x,
586
+ y
587
+ ]);
588
+ const wasLeavingRef = React.useRef(false);
589
+ const isPlacedRef = React.useRef(false);
590
+ React.useEffect(() => {
591
+ const target = isLeaving ? 0 : position;
592
+ if (target === void 0) return;
593
+ if (wasLeavingRef.current && !isLeaving) {
594
+ x.jump(0);
595
+ y.jump(0);
596
+ place.jump(target);
597
+ setIsFading(false);
598
+ } else if (prefersReducedMotion || !isPlacedRef.current) place.jump(target);
599
+ else animate(place, target, PLACE_SPRING);
600
+ isPlacedRef.current = true;
601
+ wasLeavingRef.current = isLeaving;
602
+ }, [
603
+ isLeaving,
604
+ place,
605
+ position,
606
+ prefersReducedMotion,
607
+ x,
608
+ y
609
+ ]);
610
+ const enterAtRef = React.useRef(null);
611
+ React.useEffect(() => {
612
+ if (!isStarting || position === void 0) return;
613
+ if (enterAtRef.current === null) {
614
+ const delay = prefersReducedMotion || position === 0 ? 0 : ENTER_LEAD + (position - 1) * ENTER_STAGGER;
615
+ enterAtRef.current = performance.now() + delay * 1e3;
616
+ if (delay > 0) {
617
+ enterScale.jump(ENTER_SCALE);
618
+ animate(enterScale, 1, {
619
+ ...PLACE_SPRING,
620
+ delay
621
+ });
622
+ }
623
+ } else if (position === 0) animate(enterScale, 1, PLACE_SPRING);
624
+ const wait = position === 0 ? 0 : Math.max(0, enterAtRef.current - performance.now());
625
+ let frame = 0;
626
+ const timer = window.setTimeout(() => {
627
+ frame = requestAnimationFrame(() => setIsStarting(false));
628
+ }, wait);
629
+ return () => {
630
+ window.clearTimeout(timer);
631
+ cancelAnimationFrame(frame);
632
+ };
633
+ }, [
634
+ enterScale,
635
+ isStarting,
636
+ position,
637
+ prefersReducedMotion
638
+ ]);
639
+ const { settle } = deck;
640
+ React.useEffect(() => {
641
+ const element = elementRef.current;
642
+ if (!isFront || isStarting || !element) return;
643
+ let isCancelled = false;
644
+ const report = () => {
645
+ transitionsEnd(element).then(() => {
646
+ if (!isCancelled) settle(value);
647
+ });
648
+ };
649
+ const stopListening = place.on("animationComplete", report);
650
+ if (!place.isAnimating()) report();
651
+ return () => {
652
+ isCancelled = true;
653
+ stopListening();
654
+ };
655
+ }, [
656
+ isFront,
657
+ isStarting,
658
+ place,
659
+ settle,
660
+ value
661
+ ]);
662
+ React.useEffect(() => {
663
+ if (isFront && !deck.disabled || !dragRef.current) return;
664
+ endDragRef.current?.();
665
+ setIsSwiping(false);
666
+ snapBack();
667
+ if (!isFront) releaseLift(deck.lift, prefersReducedMotion);
668
+ }, [
669
+ deck.disabled,
670
+ deck.lift,
671
+ isFront,
672
+ prefersReducedMotion,
673
+ snapBack
674
+ ]);
675
+ const release = (drag, event) => {
676
+ if (!drag.moved) {
677
+ snapBack();
678
+ return;
679
+ }
680
+ setIsSwiping(false);
681
+ draggedRef.current = true;
682
+ window.setTimeout(() => {
683
+ draggedRef.current = false;
684
+ });
685
+ if (event.type === "pointerup") drag.samples.push({
686
+ x: event.clientX,
687
+ y: event.clientY,
688
+ time: event.timeStamp
689
+ });
690
+ const velocity = releaseVelocity(drag.samples, event.timeStamp);
691
+ if (event.type === "pointerup" && !disabledRef.current) {
692
+ const ahead = lean(directionsRef.current, x.get() + velocity[0] * THROW_TIME, y.get() + velocity[1] * THROW_TIME, sizeRef.current);
693
+ if (ahead.direction && ahead.share >= 1) {
694
+ fling(ahead.direction, velocity);
695
+ return;
696
+ }
697
+ }
698
+ snapBack(velocity);
699
+ };
700
+ const handlePointerDown = (event) => {
701
+ onPointerDown?.(event);
702
+ if (event.defaultPrevented || event.button !== 0 || !isFront || deck.disabled || dragRef.current) return;
703
+ const element = event.currentTarget;
704
+ const field = event.target instanceof Element ? event.target.closest(FIELD_SELECTOR) : null;
705
+ if (field && element.contains(field)) return;
706
+ deck.lift.stop();
707
+ x.stop();
708
+ y.stop();
709
+ sizeRef.current = sizeOf(element);
710
+ const drag = {
711
+ pointerId: event.pointerId,
712
+ startX: event.clientX,
713
+ startY: event.clientY,
714
+ originX: x.get(),
715
+ originY: y.get(),
716
+ moved: false,
717
+ samples: [{
718
+ x: event.clientX,
719
+ y: event.clientY,
720
+ time: event.timeStamp
721
+ }]
722
+ };
723
+ dragRef.current = drag;
724
+ const handleMove = (moveEvent) => {
725
+ if (moveEvent.pointerId !== drag.pointerId) return;
726
+ const dx = moveEvent.clientX - drag.startX;
727
+ const dy = moveEvent.clientY - drag.startY;
728
+ if (!drag.moved) {
729
+ if (Math.hypot(dx, dy) < DRAG_SLOP) return;
730
+ drag.moved = true;
731
+ element.setPointerCapture(drag.pointerId);
732
+ setIsSwiping(true);
733
+ }
734
+ x.set(drag.originX + dx);
735
+ y.set(drag.originY + dy);
736
+ drag.samples.push({
737
+ x: moveEvent.clientX,
738
+ y: moveEvent.clientY,
739
+ time: moveEvent.timeStamp
740
+ });
741
+ if (drag.samples.length > 20) drag.samples.shift();
742
+ setDirection(lean(directionsRef.current, x.get(), y.get(), sizeRef.current).direction);
743
+ };
744
+ const handleEnd = (endEvent) => {
745
+ if (endEvent.pointerId !== drag.pointerId) return;
746
+ endDrag();
747
+ release(drag, endEvent);
748
+ };
749
+ const endDrag = () => {
750
+ window.removeEventListener("pointermove", handleMove);
751
+ window.removeEventListener("pointerup", handleEnd);
752
+ window.removeEventListener("pointercancel", handleEnd);
753
+ dragRef.current = null;
754
+ endDragRef.current = null;
755
+ };
756
+ window.addEventListener("pointermove", handleMove);
757
+ window.addEventListener("pointerup", handleEnd);
758
+ window.addEventListener("pointercancel", handleEnd);
759
+ endDragRef.current = endDrag;
760
+ };
761
+ const handleClickCapture = (event) => {
762
+ if (draggedRef.current) {
763
+ draggedRef.current = false;
764
+ event.preventDefault();
765
+ event.stopPropagation();
766
+ return;
767
+ }
768
+ onClickCapture?.(event);
769
+ };
770
+ const zIndex = isLeaving ? deck.count + 1 : position === void 0 ? void 0 : deck.count - position;
771
+ return /* @__PURE__ */ jsx("div", {
772
+ ref: mergedRef,
773
+ "data-slot": "swipe-deck-item",
774
+ role: "group",
775
+ "data-front": isFront ? "" : void 0,
776
+ "data-swiping": isSwiping ? "" : void 0,
777
+ "data-leaving": isLeaving ? "" : void 0,
778
+ "data-starting-style": isStarting ? "" : void 0,
779
+ "data-ending-style": isFading ? "" : void 0,
780
+ "data-swipe-direction": isSwiping || isLeaving ? direction : void 0,
781
+ inert: isLeaving || position !== void 0 && position > 0,
782
+ className: cn("motion-fade-lg relative col-start-1 row-start-1 rounded-lg select-none", "[--swipe-deck-slot:calc(var(--swipe-deck-place,0)_-_min(var(--swipe-deck-place,0),1)_*_var(--swipe-deck-lift,0))]", "[--swipe-deck-layout-slot:min(var(--swipe-deck-slot),calc(var(--swipe-deck-visible,4)_-_1))]", "[translate:calc(var(--swipe-deck-movement-x,0px)_+_var(--swipe-deck-layout-x,0px))_calc(var(--swipe-deck-movement-y,0px)_+_var(--swipe-deck-layout-y,0px))]", "[rotate:var(--swipe-deck-layout-rotate,0deg)] [scale:calc(var(--swipe-deck-layout-scale,1)_*_var(--swipe-deck-enter-scale,1))]", "[opacity:clamp(0,calc(var(--swipe-deck-visible,4)_-_var(--swipe-deck-slot)),1)]", "group-data-[layout=stack]/swipe-deck:origin-top", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-32px)]", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.1))]", "group-data-[layout=fan]/swipe-deck:origin-[90%_90%]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_8deg)]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.12))]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-x:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-28px)]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-28px)]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.07))]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-x:calc(var(--swipe-deck-scatter-x)_*_var(--swipe-deck-spread,0.5)_*_56px)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-scatter-y)_*_var(--swipe-deck-spread,0.5)_*_44px)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-scatter-rotate)_*_var(--swipe-deck-spread,0.5)_*_40deg)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.03))]", "ring-focus/50 group-focus-visible/swipe-deck:data-front:ring-3", "data-front:cursor-grab data-swiping:cursor-grabbing group-data-disabled/swipe-deck:data-front:cursor-auto", touchAction(directions), className),
783
+ style: {
784
+ zIndex,
785
+ "--swipe-deck-index": isLeaving ? 0 : position,
786
+ "--swipe-deck-scatter-x": scatter[0],
787
+ "--swipe-deck-scatter-y": scatter[1],
788
+ "--swipe-deck-scatter-rotate": scatter[2],
789
+ ...style
790
+ },
791
+ onPointerDown: handlePointerDown,
792
+ onClickCapture: handleClickCapture,
793
+ onDragStart: (event) => {
794
+ onDragStart?.(event);
795
+ event.preventDefault();
796
+ },
797
+ ...props
798
+ });
799
+ }
800
+ /**
801
+ * A rounded rectangle, inset half a pixel so a 1px line sits inside the box as
802
+ * a border would, with corners that are superellipses of `exponent` (2 is a
803
+ * circular arc, 4 a squircle). Returns the path and its length.
804
+ */
805
+ function outlinePath(width, height, radius, exponent) {
806
+ const inset = .5;
807
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2) - inset);
808
+ const [left, top] = [inset, inset];
809
+ const [right, bottom] = [width - inset, height - inset];
810
+ const corners = [
811
+ [
812
+ right - r,
813
+ top + r,
814
+ -Math.PI / 2
815
+ ],
816
+ [
817
+ right - r,
818
+ bottom - r,
819
+ 0
820
+ ],
821
+ [
822
+ left + r,
823
+ bottom - r,
824
+ Math.PI / 2
825
+ ],
826
+ [
827
+ left + r,
828
+ top + r,
829
+ Math.PI
830
+ ]
831
+ ];
832
+ const points = [];
833
+ for (const [cx, cy, start] of corners) for (let step = 0; step <= OUTLINE_CORNER_STEPS; step++) {
834
+ const angle = start + step / OUTLINE_CORNER_STEPS * (Math.PI / 2);
835
+ const [cos, sin] = [Math.cos(angle), Math.sin(angle)];
836
+ points.push([cx + r * Math.sign(cos) * Math.abs(cos) ** (2 / exponent), cy + r * Math.sign(sin) * Math.abs(sin) ** (2 / exponent)]);
837
+ }
838
+ const length = points.reduce((total, [x, y], index) => {
839
+ const [px, py] = points[(index + points.length - 1) % points.length];
840
+ return total + Math.hypot(x - px, y - py);
841
+ }, 0);
842
+ return {
843
+ d: `M ${points.map(([x, y]) => `${x.toFixed(2)} ${y.toFixed(2)}`).join(" L ")} Z`,
844
+ length
845
+ };
846
+ }
847
+ /**
848
+ * A 1px dashed outline with long dashes on the box it sits in. CSS ties a
849
+ * dashed border's dash length to its width, so this one is drawn, on the same
850
+ * corners as the cards: squircles where the browser draws them, arcs elsewhere.
851
+ */
852
+ function DashedOutline() {
853
+ const svgRef = React.useRef(null);
854
+ const [outline, setOutline] = React.useState({
855
+ d: "",
856
+ length: 0
857
+ });
858
+ React.useLayoutEffect(() => {
859
+ const box = svgRef.current?.parentElement;
860
+ if (!box) return;
861
+ const exponent = CSS.supports("corner-shape", "squircle") ? 4 : 2;
862
+ const draw = () => setOutline(outlinePath(box.offsetWidth, box.offsetHeight, parseFloat(getComputedStyle(box).borderTopLeftRadius), exponent));
863
+ draw();
864
+ const observer = new ResizeObserver(draw);
865
+ observer.observe(box);
866
+ return () => observer.disconnect();
867
+ }, []);
868
+ const period = 14;
869
+ const pathLength = Math.max(1, Math.round(outline.length / period)) * period;
870
+ return /* @__PURE__ */ jsx("svg", {
871
+ "data-slot": "swipe-deck-empty-outline",
872
+ ref: svgRef,
873
+ className: "pointer-events-none absolute inset-0 size-full overflow-visible",
874
+ "aria-hidden": true,
875
+ children: /* @__PURE__ */ jsx("path", {
876
+ d: outline.d,
877
+ pathLength,
878
+ strokeDasharray: `${OUTLINE_DASH} ${OUTLINE_GAP}`,
879
+ className: "stroke-shade fill-none"
880
+ })
881
+ });
882
+ }
883
+ /**
884
+ * What the deck shows once its cards run out: a message, an action (start
885
+ * over, carry on), anything you put in it. It sits where the cards sit, at
886
+ * their size, inside a dashed outline of their shape (`data-slot`
887
+ * `swipe-deck-empty-outline`, to restyle or hide). It is drawn as soon as one
888
+ * card is left and has come to rest, under that card, so dragging it shows
889
+ * what comes next; it
890
+ * can be reached (Tab, clicks) and screen readers hear it (its `aria-label`,
891
+ * or its text) once that last card has gone too.
892
+ */
893
+ function SwipeDeckEmpty({ className, children, ref, ...props }) {
894
+ const deck = useSwipeDeckContext("SwipeDeckEmpty");
895
+ const { emptyRef, focusDeck, isEmpty } = deck;
896
+ const isDrawn = deck.isReady && (deck.positions.size === 0 || deck.positions.size === 1 && deck.settledFront === deck.front);
897
+ const elementRef = React.useRef(null);
898
+ const mergedRef = React.useMemo(() => mergeRefs(elementRef, emptyRef, ref), [emptyRef, ref]);
899
+ React.useLayoutEffect(() => {
900
+ if (!isEmpty && elementRef.current?.contains(document.activeElement)) focusDeck();
901
+ }, [focusDeck, isEmpty]);
902
+ return /* @__PURE__ */ jsxs("div", {
903
+ ref: mergedRef,
904
+ "data-slot": "swipe-deck-empty",
905
+ inert: !isEmpty,
906
+ className: cn("text-muted relative col-start-1 row-start-1 flex min-h-(--swipe-deck-card-height) min-w-(--swipe-deck-card-width) flex-col items-center justify-center gap-3 rounded-lg p-6 text-center text-sm", !isDrawn && "invisible", className),
907
+ ...props,
908
+ children: [/* @__PURE__ */ jsx(DashedOutline, {}), children]
909
+ });
910
+ }
911
+ /**
912
+ * A button that sends the front card off in `direction`: the way to swipe
913
+ * without dragging. Disabled when the deck is empty or can't go that way.
914
+ */
915
+ function SwipeDeckAction({ direction, disabled, onClick, ...props }) {
916
+ const deck = useSwipeDeckContext("SwipeDeckAction");
917
+ const unavailable = deck.disabled || deck.isReady && deck.front === void 0 || !deck.directions.includes(direction);
918
+ return /* @__PURE__ */ jsx(Button, {
919
+ "data-slot": "swipe-deck-action",
920
+ "data-swipe-direction": direction,
921
+ disabled: disabled || unavailable,
922
+ focusableWhenDisabled: true,
923
+ onClick: (event) => {
924
+ onClick?.(event);
925
+ if (!event.defaultPrevented) deck.swipeFront(direction);
926
+ },
927
+ ...props
928
+ });
929
+ }
930
+ //#endregion
931
+ export { SwipeDeck, SwipeDeckAction, SwipeDeckEmpty, SwipeDeckItem };