solid-drift 0.8.0 → 0.10.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/README.md CHANGED
@@ -345,6 +345,43 @@ const { rotateX, rotateY } = createTilt(() => card, { maxAngle: 12 })
345
345
 
346
346
  Returns `{ rotateX, rotateY }`: spring-smoothed tilt in degrees. SSR-safe and reduced-motion safe: both return constant `0` accessors.
347
347
 
348
+ ### `createDrag(ref, options?)`
349
+
350
+ Pointer drag with spring physics, constraints, and momentum. The gesture workhorse: draggable cards, sliders, bottom-sheet handles, sortable rows. While the pointer is down the element tracks it 1:1; on release it glides with inertia and springs into its constraints, stretching elastically past the edges while dragged. Set `touch-action: none` on the draggable element so touch drags do not fight the page scroll. For the physics-toy flavor (exponential friction plus bouncing off walls), see `createFling` instead.
351
+
352
+ ```tsx
353
+ import { createDrag } from "solid-drift"
354
+
355
+ let card!: HTMLDivElement
356
+ const { x, y, status } = createDrag(() => card, {
357
+ constraints: { left: 0, right: 300, top: 0, bottom: 0 },
358
+ elastic: 0.4,
359
+ })
360
+ <div
361
+ ref={card}
362
+ style={{
363
+ transform: `translate(${x()}px, ${y()}px)`,
364
+ "touch-action": "none",
365
+ cursor: status() === "dragging" ? "grabbing" : "grab",
366
+ }}
367
+ >
368
+ Drag me
369
+ </div>
370
+ ```
371
+
372
+ | Option | Default | Description |
373
+ | ------------- | -------------------------------- | ------------------------------------------------------------------ |
374
+ | `axis` | `"both"` | `"x"`, `"y"`, or `"both"` |
375
+ | `constraints` | none | Bounds in px relative to the drag origin; release settles inside |
376
+ | `elastic` | `0.35` | Overshoot past constraints while dragging, 0 (hard stop) to 1 |
377
+ | `momentum` | `true` | Glide with inertia after release |
378
+ | `inertia` | `0.2` | Seconds of release velocity projected into the settle target |
379
+ | `spring` | `{ stiffness: 300, damping: 32 }` | Spring physics for the settle after release |
380
+ | `onDragStart` | none | Called when the pointer grabs the element |
381
+ | `onDragEnd` | none | Called on release with `{ x, y, velocityX, velocityY }` (px/s) |
382
+
383
+ Returns `{ x, y, status }`: the drag offset in pixels and `status` (`"idle"`, `"dragging"`, `"settling"`). SSR-safe: everything rests at 0. Under reduced motion the drag still tracks the pointer (direct manipulation is not animation) but release snaps instantly to the constrained target with no glide.
384
+
348
385
  ### `createTrail(source, options?)`
349
386
 
350
387
  A signal that replays another signal's past: it returns the value the source had `delay` milliseconds ago, interpolated between samples. Chain trails off one source for follower effects (a cursor with a comet tail, cascading highlights), or trail a scroll progress for a delayed echo of the page. The trail catches up and parks exactly on the latest value when the source rests. The follow loop runs only while the trail is behind.
@@ -430,6 +467,41 @@ const shuffle = () =>
430
467
 
431
468
  Returns `{ stop, finished }`: `stop()` halts mid-flight and restores the original transform. SSR-safe and reduced-motion safe: the mutation runs with no animation. If the element does not move, no animation runs either.
432
469
 
470
+ ### `createSharedLayout(ref, options)`
471
+
472
+ Shared-element transition across mounts, the `layoutId` magic. Give the same `id` to elements that represent the same thing in different places: a tab pill, a card that opens into a detail view, a thumbnail that becomes a hero image. When the old element unmounts its rect is remembered; when the new one mounts it starts at the donor's rect and springs home, sliding and morphing into place. This is cross-mount FLIP: where `animateFlip` inverts the delta around a mutation on one element, `createSharedLayout` inverts the delta between two elements that share an identity. One active carrier per id.
473
+
474
+ ```tsx
475
+ import { createSharedLayout } from "solid-drift"
476
+
477
+ function TabPill(props: { id: string }) {
478
+ let pill!: HTMLDivElement
479
+ const { x, y, scaleX, scaleY } = createSharedLayout(() => pill, {
480
+ id: props.id,
481
+ })
482
+ return (
483
+ <div
484
+ ref={pill}
485
+ style={{
486
+ transform: `translate(${x()}px, ${y()}px) scale(${scaleX()}, ${scaleY()})`,
487
+ }}
488
+ />
489
+ )
490
+ }
491
+
492
+ // Only one pill is mounted at a time; it glides between tabs.
493
+ <Show when={tab() === "a"}><TabPill id="pill" /></Show>
494
+ <Show when={tab() === "b"}><TabPill id="pill" /></Show>
495
+ ```
496
+
497
+ | Option | Default | Description |
498
+ | -------- | -------------------------------- | --------------------------------------------------------------- |
499
+ | `id` | required | Shared identity; elements with the same id hand off to each other |
500
+ | `spring` | `{ stiffness: 260, damping: 30 }` | Spring physics for the handoff flight |
501
+ | `scale` | `true` | Also morph the size delta as scale |
502
+
503
+ Returns `{ x, y, scaleX, scaleY, flying }`: the corrective transform and `flying`, true while the handoff runs. SSR-safe and reduced-motion safe: the element simply appears, with no flight.
504
+
433
505
  ### `springPresets`
434
506
 
435
507
  Named spring configurations for common feels. Spread into `createSpring`, `createMagnetic`, or `createTilt` options.
package/dist/flip.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { type Accessor } from "solid-js";
1
2
  import { type AnimateOptions, type AnimationControls } from "./animate.js";
3
+ import type { SpringOptions } from "./spring.js";
2
4
  export interface FlipOptions {
3
5
  /** Duration in milliseconds. Default 400. */
4
6
  duration?: number;
@@ -42,3 +44,74 @@ export interface FlipOptions {
42
44
  * ```
43
45
  */
44
46
  export declare function animateFlip(ref: () => Element | null | undefined, mutate: () => void, options?: FlipOptions): AnimationControls;
47
+ export interface SharedLayoutOptions {
48
+ /**
49
+ * Shared identity. Elements with the same id hand off to each other
50
+ * across mounts: the newcomer flies in from the donor's last rect.
51
+ */
52
+ id: string;
53
+ /**
54
+ * Spring physics for the handoff flight. Default
55
+ * `{ stiffness: 260, damping: 30 }`.
56
+ */
57
+ spring?: SpringOptions;
58
+ /**
59
+ * Also morph the size delta as scale, so growing or shrinking elements
60
+ * morph instead of just sliding. Default true.
61
+ */
62
+ scale?: boolean;
63
+ }
64
+ export interface SharedLayoutResult {
65
+ /** Horizontal corrective offset in pixels. */
66
+ x: Accessor<number>;
67
+ /** Vertical corrective offset in pixels. */
68
+ y: Accessor<number>;
69
+ /** Horizontal corrective scale. */
70
+ scaleX: Accessor<number>;
71
+ /** Vertical corrective scale. */
72
+ scaleY: Accessor<number>;
73
+ /** True while the handoff flight is running. */
74
+ flying: Accessor<boolean>;
75
+ }
76
+ /**
77
+ * Shared-element transition across mounts, the `layoutId` magic.
78
+ *
79
+ * Give the same `id` to elements that represent the same thing in
80
+ * different places: a tab pill, a card that opens into a detail view,
81
+ * a thumbnail that becomes a hero image. When the old element unmounts
82
+ * its rect is remembered; when the new one mounts it starts at the
83
+ * donor's rect and springs home, sliding and morphing into place.
84
+ *
85
+ * This is cross-mount FLIP: where `animateFlip` inverts the delta
86
+ * around a mutation on one element, `createSharedLayout` inverts the
87
+ * delta between two elements that share an identity. Bind the returned
88
+ * values as a transform on the mounted element.
89
+ *
90
+ * SSR-safe and reduced-motion safe: the element simply appears, with
91
+ * no flight. One active carrier per id: the first mount consumes the
92
+ * parked donor rect.
93
+ *
94
+ * ```tsx
95
+ * import { createSharedLayout } from "solid-drift"
96
+ *
97
+ * function TabPill(props: { id: string }) {
98
+ * let pill!: HTMLDivElement
99
+ * const { x, y, scaleX, scaleY } = createSharedLayout(() => pill, {
100
+ * id: props.id,
101
+ * })
102
+ * return (
103
+ * <div
104
+ * ref={pill}
105
+ * style={{
106
+ * transform: `translate(${x()}px, ${y()}px) scale(${scaleX()}, ${scaleY()})`,
107
+ * }}
108
+ * />
109
+ * )
110
+ * }
111
+ *
112
+ * // Only one pill is mounted at a time; it glides between tabs.
113
+ * <Show when={tab() === "a"}><TabPill id="pill" /></Show>
114
+ * <Show when={tab() === "b"}><TabPill id="pill" /></Show>
115
+ * ```
116
+ */
117
+ export declare function createSharedLayout(ref: () => Element | null | undefined, options: SharedLayoutOptions): SharedLayoutResult;
package/dist/flip.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { createEffect, createSignal, onCleanup, } from "solid-js";
1
2
  import { animate, } from "./animate.js";
3
+ import { now, schedule } from "./engine.js";
2
4
  import { prefersReducedMotion } from "./reduced-motion.js";
3
5
  /**
4
6
  * FLIP layout animation around a DOM mutation.
@@ -106,3 +108,158 @@ export function animateFlip(ref, mutate, options = {}) {
106
108
  }));
107
109
  return controls;
108
110
  }
111
+ /**
112
+ * Donor registry for shared-element handoffs. When an element carrying a
113
+ * shared id unmounts, its last rect is parked here; the next element that
114
+ * mounts with the same id consumes it and flies in from that rect.
115
+ * One active carrier per id: consumption deletes the entry.
116
+ */
117
+ const donors = new Map();
118
+ function rectOf(el) {
119
+ const r = el.getBoundingClientRect();
120
+ return { left: r.left, top: r.top, width: r.width, height: r.height };
121
+ }
122
+ const zero = () => 0;
123
+ const one = () => 1;
124
+ const no = () => false;
125
+ /**
126
+ * Shared-element transition across mounts, the `layoutId` magic.
127
+ *
128
+ * Give the same `id` to elements that represent the same thing in
129
+ * different places: a tab pill, a card that opens into a detail view,
130
+ * a thumbnail that becomes a hero image. When the old element unmounts
131
+ * its rect is remembered; when the new one mounts it starts at the
132
+ * donor's rect and springs home, sliding and morphing into place.
133
+ *
134
+ * This is cross-mount FLIP: where `animateFlip` inverts the delta
135
+ * around a mutation on one element, `createSharedLayout` inverts the
136
+ * delta between two elements that share an identity. Bind the returned
137
+ * values as a transform on the mounted element.
138
+ *
139
+ * SSR-safe and reduced-motion safe: the element simply appears, with
140
+ * no flight. One active carrier per id: the first mount consumes the
141
+ * parked donor rect.
142
+ *
143
+ * ```tsx
144
+ * import { createSharedLayout } from "solid-drift"
145
+ *
146
+ * function TabPill(props: { id: string }) {
147
+ * let pill!: HTMLDivElement
148
+ * const { x, y, scaleX, scaleY } = createSharedLayout(() => pill, {
149
+ * id: props.id,
150
+ * })
151
+ * return (
152
+ * <div
153
+ * ref={pill}
154
+ * style={{
155
+ * transform: `translate(${x()}px, ${y()}px) scale(${scaleX()}, ${scaleY()})`,
156
+ * }}
157
+ * />
158
+ * )
159
+ * }
160
+ *
161
+ * // Only one pill is mounted at a time; it glides between tabs.
162
+ * <Show when={tab() === "a"}><TabPill id="pill" /></Show>
163
+ * <Show when={tab() === "b"}><TabPill id="pill" /></Show>
164
+ * ```
165
+ */
166
+ export function createSharedLayout(ref, options) {
167
+ const { id, spring = {}, scale = true } = options;
168
+ if (typeof window === "undefined") {
169
+ return { x: zero, y: zero, scaleX: one, scaleY: one, flying: no };
170
+ }
171
+ const [x, setX] = createSignal(0);
172
+ const [y, setY] = createSignal(0);
173
+ const [scaleX, setScaleX] = createSignal(1);
174
+ const [scaleY, setScaleY] = createSignal(1);
175
+ const [flying, setFlying] = createSignal(false);
176
+ let cancelFlight = null;
177
+ const stopFlight = () => {
178
+ cancelFlight?.();
179
+ cancelFlight = null;
180
+ };
181
+ /** Spring flight from the inverted delta back to identity. */
182
+ const flyFrom = (dx, dy, sx, sy) => {
183
+ const { stiffness = 260, damping = 30 } = spring;
184
+ let cx = dx;
185
+ let cy = dy;
186
+ let csx = sx;
187
+ let csy = sy;
188
+ let vx = 0;
189
+ let vy = 0;
190
+ let vsx = 0;
191
+ let vsy = 0;
192
+ let last = now();
193
+ setX(cx);
194
+ setY(cy);
195
+ setScaleX(csx);
196
+ setScaleY(csy);
197
+ setFlying(true);
198
+ stopFlight();
199
+ const task = (t) => {
200
+ const dt = Math.min(Math.max((t - last) / 1000, 0), 0.064);
201
+ last = t;
202
+ // Semi-implicit Euler per channel, same integrator as createSpring.
203
+ vx += (-stiffness * cx - damping * vx) * dt;
204
+ vy += (-stiffness * cy - damping * vy) * dt;
205
+ vsx += (-stiffness * (csx - 1) - damping * vsx) * dt;
206
+ vsy += (-stiffness * (csy - 1) - damping * vsy) * dt;
207
+ cx += vx * dt;
208
+ cy += vy * dt;
209
+ csx += vsx * dt;
210
+ csy += vsy * dt;
211
+ setX(cx);
212
+ setY(cy);
213
+ setScaleX(csx);
214
+ setScaleY(csy);
215
+ const settled = Math.hypot(cx, cy) < 0.5 &&
216
+ Math.abs(csx - 1) < 0.003 &&
217
+ Math.abs(csy - 1) < 0.003 &&
218
+ Math.hypot(vx, vy) < 20 &&
219
+ Math.hypot(vsx, vsy) < 0.05;
220
+ if (settled) {
221
+ setX(0);
222
+ setY(0);
223
+ setScaleX(1);
224
+ setScaleY(1);
225
+ setFlying(false);
226
+ cancelFlight = null;
227
+ return false;
228
+ }
229
+ return true;
230
+ };
231
+ cancelFlight = schedule(task);
232
+ };
233
+ // Late-bound refs (Solid assigns `ref` after mount) still hand off.
234
+ createEffect(() => {
235
+ const el = ref();
236
+ if (!el)
237
+ return;
238
+ const mountRect = rectOf(el);
239
+ const donor = donors.get(id);
240
+ donors.delete(id);
241
+ if (donor && !prefersReducedMotion()) {
242
+ const own = mountRect;
243
+ const dx = donor.left - own.left;
244
+ const dy = donor.top - own.top;
245
+ const sx = scale && own.width > 0 ? donor.width / own.width : 1;
246
+ const sy = scale && own.height > 0 ? donor.height / own.height : 1;
247
+ const moved = Math.abs(dx) >= 0.5 ||
248
+ Math.abs(dy) >= 0.5 ||
249
+ Math.abs(sx - 1) >= 0.002 ||
250
+ Math.abs(sy - 1) >= 0.002;
251
+ if (moved)
252
+ flyFrom(dx, dy, sx, sy);
253
+ }
254
+ onCleanup(() => {
255
+ // In <Show>/<Switch>/&& branches the cleanup runs while the node
256
+ // is still attached, so this is the live rect. If the node was
257
+ // already detached (all-zero rect), keep the mount snapshot.
258
+ const r = rectOf(el);
259
+ const valid = r.width > 0 || r.height > 0 || r.left !== 0 || r.top !== 0;
260
+ donors.set(id, valid ? r : mountRect);
261
+ });
262
+ });
263
+ onCleanup(() => stopFlight());
264
+ return { x, y, scaleX, scaleY, flying };
265
+ }
@@ -0,0 +1,108 @@
1
+ import { type Accessor } from "solid-js";
2
+ import type { SpringOptions } from "./spring.js";
3
+ type MaybeElement = () => Element | null | undefined;
4
+ /** Lifecycle of a drag gesture. */
5
+ export type DragStatus = "idle" | "dragging" | "settling";
6
+ /** Which axes the drag follows. Default "both". */
7
+ export type DragAxis = "x" | "y" | "both";
8
+ /**
9
+ * Drag bounds in pixels, relative to the drag origin (the element's
10
+ * resting position). Omit an edge for no limit on that side.
11
+ */
12
+ export interface DragConstraints {
13
+ left?: number;
14
+ top?: number;
15
+ right?: number;
16
+ bottom?: number;
17
+ }
18
+ /** Snapshot delivered to `onDragEnd` when the pointer lifts. */
19
+ export interface DragEndInfo {
20
+ /** Final drag offset in pixels. */
21
+ x: number;
22
+ /** Final drag offset in pixels. */
23
+ y: number;
24
+ /** Release velocity in px/s. */
25
+ velocityX: number;
26
+ /** Release velocity in px/s. */
27
+ velocityY: number;
28
+ }
29
+ export interface DragOptions {
30
+ /** Which axes the drag follows. Default "both". */
31
+ axis?: DragAxis;
32
+ /**
33
+ * Bounds in pixels, relative to the drag origin. Omit for free drag.
34
+ * On release the element settles back inside the bounds.
35
+ */
36
+ constraints?: DragConstraints;
37
+ /**
38
+ * Overshoot allowed beyond the constraints while dragging, 0 to 1.
39
+ * 0 is a hard stop, 1 removes all resistance. Default 0.35.
40
+ */
41
+ elastic?: number;
42
+ /** Keep gliding after release with inertia. Default true. */
43
+ momentum?: boolean;
44
+ /**
45
+ * Seconds of release velocity projected into the settle target.
46
+ * Higher values glide further. Default 0.2.
47
+ */
48
+ inertia?: number;
49
+ /**
50
+ * Spring physics for the settle after release. Default
51
+ * `{ stiffness: 300, damping: 32 }`.
52
+ */
53
+ spring?: SpringOptions;
54
+ /** Called when the pointer grabs the element. */
55
+ onDragStart?: () => void;
56
+ /** Called when the pointer lifts, with the release snapshot. */
57
+ onDragEnd?: (info: DragEndInfo) => void;
58
+ }
59
+ export interface DragControls {
60
+ /** Horizontal offset in pixels from the resting position. */
61
+ x: Accessor<number>;
62
+ /** Vertical offset in pixels from the resting position. */
63
+ y: Accessor<number>;
64
+ /** `idle`, `dragging` while the pointer is down, `settling` after release. */
65
+ status: Accessor<DragStatus>;
66
+ }
67
+ /**
68
+ * Pointer drag with spring physics, constraints, and momentum.
69
+ *
70
+ * The gesture workhorse: draggable cards, sliders, bottom-sheet handles,
71
+ * sortable rows. While the pointer is down the element tracks it 1:1;
72
+ * on release it glides with inertia and springs into its constraints,
73
+ * stretching elastically past the edges while dragged.
74
+ *
75
+ * This is the framer-motion `drag` vocabulary rebuilt signal-native:
76
+ * `axis` locks direction, `constraints` bounds the travel, `elastic`
77
+ * controls the overshoot, and `momentum` toggles the release glide.
78
+ * For the physics-toy flavor (friction plus bouncing off walls), see
79
+ * `createFling` instead.
80
+ *
81
+ * Set `touch-action: none` on the draggable element so touch drags do
82
+ * not fight the page scroll.
83
+ *
84
+ * SSR-safe and reduced-motion aware: on the server everything rests at
85
+ * 0; under reduced motion the drag still tracks the pointer (direct
86
+ * manipulation is not animation) but release snaps instantly to the
87
+ * constrained target with no glide.
88
+ *
89
+ * ```tsx
90
+ * let card!: HTMLDivElement
91
+ * const { x, y, status } = createDrag(() => card, {
92
+ * constraints: { left: 0, right: 300, top: 0, bottom: 0 },
93
+ * elastic: 0.4,
94
+ * })
95
+ * <div
96
+ * ref={card}
97
+ * style={{
98
+ * transform: `translate(${x()}px, ${y()}px)`,
99
+ * "touch-action": "none",
100
+ * cursor: status() === "dragging" ? "grabbing" : "grab",
101
+ * }}
102
+ * >
103
+ * Drag me
104
+ * </div>
105
+ * ```
106
+ */
107
+ export declare function createDrag(ref: MaybeElement, options?: DragOptions): DragControls;
108
+ export {};
@@ -0,0 +1,207 @@
1
+ import { createEffect, createSignal, onCleanup, untrack, } from "solid-js";
2
+ import { now, schedule } from "./engine.js";
3
+ import { prefersReducedMotion } from "./reduced-motion.js";
4
+ const zero = () => 0;
5
+ const idle = () => "idle";
6
+ /**
7
+ * Pointer drag with spring physics, constraints, and momentum.
8
+ *
9
+ * The gesture workhorse: draggable cards, sliders, bottom-sheet handles,
10
+ * sortable rows. While the pointer is down the element tracks it 1:1;
11
+ * on release it glides with inertia and springs into its constraints,
12
+ * stretching elastically past the edges while dragged.
13
+ *
14
+ * This is the framer-motion `drag` vocabulary rebuilt signal-native:
15
+ * `axis` locks direction, `constraints` bounds the travel, `elastic`
16
+ * controls the overshoot, and `momentum` toggles the release glide.
17
+ * For the physics-toy flavor (friction plus bouncing off walls), see
18
+ * `createFling` instead.
19
+ *
20
+ * Set `touch-action: none` on the draggable element so touch drags do
21
+ * not fight the page scroll.
22
+ *
23
+ * SSR-safe and reduced-motion aware: on the server everything rests at
24
+ * 0; under reduced motion the drag still tracks the pointer (direct
25
+ * manipulation is not animation) but release snaps instantly to the
26
+ * constrained target with no glide.
27
+ *
28
+ * ```tsx
29
+ * let card!: HTMLDivElement
30
+ * const { x, y, status } = createDrag(() => card, {
31
+ * constraints: { left: 0, right: 300, top: 0, bottom: 0 },
32
+ * elastic: 0.4,
33
+ * })
34
+ * <div
35
+ * ref={card}
36
+ * style={{
37
+ * transform: `translate(${x()}px, ${y()}px)`,
38
+ * "touch-action": "none",
39
+ * cursor: status() === "dragging" ? "grabbing" : "grab",
40
+ * }}
41
+ * >
42
+ * Drag me
43
+ * </div>
44
+ * ```
45
+ */
46
+ export function createDrag(ref, options = {}) {
47
+ if (typeof window === "undefined") {
48
+ return { x: zero, y: zero, status: idle };
49
+ }
50
+ const { axis = "both", constraints, elastic = 0.35, momentum = true, inertia = 0.2, spring = {}, onDragStart, onDragEnd, } = options;
51
+ const [x, setX] = createSignal(0);
52
+ const [y, setY] = createSignal(0);
53
+ const [status, setStatus] = createSignal("idle");
54
+ let cancelSettle = null;
55
+ const clampAxis = (value, min, max) => {
56
+ if (min !== undefined && value < min)
57
+ return min;
58
+ if (max !== undefined && value > max)
59
+ return max;
60
+ return value;
61
+ };
62
+ /** Elastic resistance past the constraints while dragging. */
63
+ const applyConstraints = (px, py) => {
64
+ if (!constraints)
65
+ return [px, py];
66
+ const { left, top, right, bottom } = constraints;
67
+ let ox = px;
68
+ let oy = py;
69
+ if (left !== undefined && ox < left)
70
+ ox = left + (ox - left) * elastic;
71
+ if (right !== undefined && ox > right)
72
+ ox = right + (ox - right) * elastic;
73
+ if (top !== undefined && oy < top)
74
+ oy = top + (oy - top) * elastic;
75
+ if (bottom !== undefined && oy > bottom)
76
+ oy = bottom + (oy - bottom) * elastic;
77
+ return [ox, oy];
78
+ };
79
+ const hardClamp = (px, py) => {
80
+ if (!constraints)
81
+ return [px, py];
82
+ const { left, top, right, bottom } = constraints;
83
+ return [clampAxis(px, left, right), clampAxis(py, top, bottom)];
84
+ };
85
+ const stopSettle = () => {
86
+ cancelSettle?.();
87
+ cancelSettle = null;
88
+ };
89
+ /** Spring glide toward the target, seeded with the release velocity. */
90
+ const settleTo = (tx, ty, vx0, vy0) => {
91
+ const { stiffness = 300, damping = 32 } = spring;
92
+ let cx = untrack(x);
93
+ let cy = untrack(y);
94
+ let vx = vx0;
95
+ let vy = vy0;
96
+ let last = now();
97
+ setStatus("settling");
98
+ stopSettle();
99
+ const task = (t) => {
100
+ const dt = Math.min(Math.max((t - last) / 1000, 0), 0.064);
101
+ last = t;
102
+ // Semi-implicit Euler toward the target, same integrator as createSpring.
103
+ vx += (-stiffness * (cx - tx) - damping * vx) * dt;
104
+ vy += (-stiffness * (cy - ty) - damping * vy) * dt;
105
+ cx += vx * dt;
106
+ cy += vy * dt;
107
+ setX(cx);
108
+ setY(cy);
109
+ const settled = Math.hypot(cx - tx, cy - ty) < 0.5 && Math.hypot(vx, vy) < 20;
110
+ if (settled) {
111
+ setX(tx);
112
+ setY(ty);
113
+ setStatus("idle");
114
+ cancelSettle = null;
115
+ return false;
116
+ }
117
+ return true;
118
+ };
119
+ cancelSettle = schedule(task);
120
+ };
121
+ const onDown = (event) => {
122
+ if (event.isPrimary === false)
123
+ return;
124
+ const el = ref();
125
+ if (!el)
126
+ return;
127
+ stopSettle();
128
+ const startPX = event.clientX;
129
+ const startPY = event.clientY;
130
+ const startX = untrack(x);
131
+ const startY = untrack(y);
132
+ const samples = [
133
+ [now(), startX, startY],
134
+ ];
135
+ setStatus("dragging");
136
+ onDragStart?.();
137
+ const move = (ev) => {
138
+ if (ev.isPrimary === false)
139
+ return;
140
+ let nx = startX + (ev.clientX - startPX);
141
+ let ny = startY + (ev.clientY - startPY);
142
+ if (axis === "x")
143
+ ny = startY;
144
+ if (axis === "y")
145
+ nx = startX;
146
+ [nx, ny] = applyConstraints(nx, ny);
147
+ setX(nx);
148
+ setY(ny);
149
+ samples.push([now(), nx, ny]);
150
+ if (samples.length > 8)
151
+ samples.shift();
152
+ };
153
+ const up = () => {
154
+ window.removeEventListener("pointermove", move);
155
+ window.removeEventListener("pointerup", up);
156
+ window.removeEventListener("pointercancel", up);
157
+ // Release velocity from the last ~120ms of the drag.
158
+ const t = now();
159
+ let oi = 0;
160
+ while (oi < samples.length - 1 && t - samples[oi][0] > 120)
161
+ oi++;
162
+ const [ot, ox, oy] = samples[oi];
163
+ const [lt, lx, ly] = samples[samples.length - 1];
164
+ const dt = Math.max(lt - ot, 1) / 1000;
165
+ const velocityX = ((lx - ox) / dt) * (axis === "y" ? 0 : 1);
166
+ const velocityY = ((ly - oy) / dt) * (axis === "x" ? 0 : 1);
167
+ const releaseX = untrack(x);
168
+ const releaseY = untrack(y);
169
+ onDragEnd?.({ x: releaseX, y: releaseY, velocityX, velocityY });
170
+ if (prefersReducedMotion()) {
171
+ // Direct manipulation still works, but the release snaps
172
+ // instantly to the constrained target: no glide, no spring.
173
+ const [cx, cy] = hardClamp(releaseX, releaseY);
174
+ setX(cx);
175
+ setY(cy);
176
+ setStatus("idle");
177
+ return;
178
+ }
179
+ // Project the release velocity forward, then spring to the
180
+ // clamped target. Overshoot past the constraints snaps back
181
+ // elastically because the target sits on the edge.
182
+ let tx = releaseX;
183
+ let ty = releaseY;
184
+ if (momentum) {
185
+ tx += velocityX * inertia;
186
+ ty += velocityY * inertia;
187
+ }
188
+ [tx, ty] = hardClamp(tx, ty);
189
+ const vx0 = momentum ? velocityX : 0;
190
+ const vy0 = momentum ? velocityY : 0;
191
+ settleTo(tx, ty, vx0, vy0);
192
+ };
193
+ window.addEventListener("pointermove", move, { passive: true });
194
+ window.addEventListener("pointerup", up);
195
+ window.addEventListener("pointercancel", up);
196
+ };
197
+ // Late-bound refs (Solid assigns `ref` after mount) still get the grab.
198
+ createEffect(() => {
199
+ const el = ref();
200
+ if (!el)
201
+ return;
202
+ el.addEventListener("pointerdown", onDown);
203
+ onCleanup(() => el.removeEventListener("pointerdown", onDown));
204
+ });
205
+ onCleanup(() => stopSettle());
206
+ return { x, y, status };
207
+ }
package/dist/index.d.ts CHANGED
@@ -19,13 +19,14 @@ export { createVelocity, type VelocityOptions } from "./velocity.js";
19
19
  export { createMagnetic, type MagneticOptions, type MagneticResult, createTilt, type TiltOptions, type TiltResult, } from "./pointer.js";
20
20
  export { createTrail, type TrailOptions } from "./trail.js";
21
21
  export { createTimeline, type TimelineStep, type TimelineStatus, type TimelineControls, } from "./timeline.js";
22
- export { animateFlip, type FlipOptions } from "./flip.js";
22
+ export { animateFlip, type FlipOptions, createSharedLayout, type SharedLayoutOptions, type SharedLayoutResult, } from "./flip.js";
23
23
  export { createSquashStretch, type SquashStretchOptions, type SquashStretchResult, createFollowThrough, type FollowThroughOptions, createAnticipation, type AnticipationOptions, createWobble, type WobbleOptions, type WobbleResult, } from "./cartoon.js";
24
24
  export { createGravity, type GravityOptions, type GravityResult, createPendulum, type PendulumOptions, type PendulumResult, createFling, type FlingOptions, type FlingResult, } from "./physics.js";
25
25
  export { createFontSwap, type FontSwapOptions, type FontSwapResult, createTyping, type TypingOptions, type TypingResult, createTextPhysics, type TextPhysicsOptions, type TextPhysicsResult, createTextTunnel, type TextTunnelOptions, type TextTunnelResult, createTextCutout, type TextCutoutOptions, createTextGradient, type TextGradientOptions, createTextScramble, type TextScrambleOptions, type TextScrambleResult, createTextWave, type TextWaveOptions, } from "./typography.js";
26
26
  export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, type Easing, type EasingName, } from "./easing.js";
27
27
  export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
28
28
  export type { KineticTypeFrom, KineticTypeOptions, KineticTypeStatus, KineticTypeControls, MotionScene, ScenePlayerStatus, ScenePlayerControls, ShowreelScene, ShowreelSceneKind, CameraKeyframe, CameraOptions, ColorShiftOptions, ColorShiftStatus, ColorShiftControls, TransitionType, TransitionDirection, TransitionOptions, TransitionLayerStyle, TransitionStatus, TransitionControls, BeatOptions, BeatStatus, BeatControls, BeatCutOptions, } from "./motion.js";
29
+ export { createDrag, type DragStatus, type DragAxis, type DragConstraints, type DragEndInfo, type DragOptions, type DragControls, } from "./gesture.js";
29
30
  export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
30
31
  export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerControls, } from "./ai.js";
31
32
  export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
package/dist/index.js CHANGED
@@ -19,11 +19,12 @@ export { createVelocity } from "./velocity.js";
19
19
  export { createMagnetic, createTilt, } from "./pointer.js";
20
20
  export { createTrail } from "./trail.js";
21
21
  export { createTimeline, } from "./timeline.js";
22
- export { animateFlip } from "./flip.js";
22
+ export { animateFlip, createSharedLayout, } from "./flip.js";
23
23
  export { createSquashStretch, createFollowThrough, createAnticipation, createWobble, } from "./cartoon.js";
24
24
  export { createGravity, createPendulum, createFling, } from "./physics.js";
25
25
  export { createFontSwap, createTyping, createTextPhysics, createTextTunnel, createTextCutout, createTextGradient, createTextScramble, createTextWave, } from "./typography.js";
26
26
  export { easings, cubicBezier, linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeOutExpo, easeOutBack, easeInBack, easeInOutBack, easeOutElastic, easeOutBounce, resolveEasing, } from "./easing.js";
27
27
  export { createKineticType, createScenePlayer, createShowreel, createCamera, createColorShift, createTransition, createBeat, createBeatCuts, } from "./motion.js";
28
+ export { createDrag, } from "./gesture.js";
28
29
  export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
29
30
  export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.8.0"
46
+ "version": "0.10.0"
47
47
  }