r3f-motion 1.0.5 → 1.0.6

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/dist/cjs/index.js CHANGED
@@ -4,9 +4,29 @@
4
4
  var tslib = require('tslib');
5
5
  var react = require('react');
6
6
  var motion$1 = require('motion');
7
- var jsxRuntime = require('react/jsx-runtime');
8
7
  var fiber = require('@react-three/fiber');
9
- var three = require('three');
8
+ var THREE = require('three');
9
+ var jsxRuntime = require('react/jsx-runtime');
10
+ var react$1 = require('motion/react');
11
+
12
+ function _interopNamespaceDefault(e) {
13
+ var n = Object.create(null);
14
+ if (e) {
15
+ Object.keys(e).forEach(function (k) {
16
+ if (k !== 'default') {
17
+ var d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: function () { return e[k]; }
21
+ });
22
+ }
23
+ });
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
10
30
 
11
31
  const PresenceContext = react.createContext(null);
12
32
  /**
@@ -249,6 +269,389 @@ function useTap(isStatic, props, options) {
249
269
  };
250
270
  }
251
271
 
272
+ // World-space units per CSS pixel at the given depth from the camera.
273
+ const getWorldPerPixel = (camera, depth, viewportHeight) => {
274
+ var _a;
275
+ const ortho = camera;
276
+ if (ortho.isOrthographicCamera) {
277
+ return ((ortho.top - ortho.bottom) / (viewportHeight * (ortho.zoom || 1)));
278
+ }
279
+ const persp = camera;
280
+ const fov = ((_a = persp.fov) !== null && _a !== void 0 ? _a : 60) * (Math.PI / 180);
281
+ return (2 * Math.tan(fov / 2) * depth) / viewportHeight;
282
+ };
283
+ const clampWithElastic = (value, min, max, elastic) => {
284
+ if (min !== undefined && value < min)
285
+ return min + (value - min) * elastic;
286
+ if (max !== undefined && value > max)
287
+ return max + (value - max) * elastic;
288
+ return value;
289
+ };
290
+ function useDrag(isStatic, props, options) {
291
+ const { camera, gl } = fiber.useThree();
292
+ const { drag, whileDrag, onDragStart, onPointerDown, transition } = props;
293
+ const { instanceRef, captureInstanceState, buildTargetFromState, animateToTarget, resolveVariant, stopAnimation, } = options;
294
+ const isDraggingRef = react.useRef(false);
295
+ const dragStartPointerRef = react.useRef({ x: 0, y: 0 });
296
+ const dragStartPosRef = react.useRef(new THREE__namespace.Vector3());
297
+ const cameraRightRef = react.useRef(new THREE__namespace.Vector3());
298
+ const cameraUpRef = react.useRef(new THREE__namespace.Vector3());
299
+ const worldPerPixelRef = react.useRef(0);
300
+ const targetPosRef = react.useRef(new THREE__namespace.Vector3());
301
+ const lastPosRef = react.useRef(new THREE__namespace.Vector3());
302
+ const velocityRef = react.useRef(new THREE__namespace.Vector3());
303
+ const lastDeltaRef = react.useRef(new THREE__namespace.Vector3());
304
+ const lastTimeRef = react.useRef(0);
305
+ const preDragStateRef = react.useRef(null);
306
+ const pointerIdRef = react.useRef(null);
307
+ const springStateRef = react.useRef(null);
308
+ const lastFrameTimeRef = react.useRef(0);
309
+ const propsRef = react.useRef(props);
310
+ propsRef.current = props;
311
+ fiber.useFrame(() => {
312
+ var _a;
313
+ const instance = instanceRef.current;
314
+ if (!instance)
315
+ return;
316
+ const obj = instance;
317
+ // Phase 1: dragging — apply the drag target
318
+ if (isDraggingRef.current) {
319
+ const t = targetPosRef.current;
320
+ obj.position.set(t.x, t.y, t.z);
321
+ return;
322
+ }
323
+ // Phase 2: spring (momentum / snap-to-origin)
324
+ const s = springStateRef.current;
325
+ if (!s || !s.active)
326
+ return;
327
+ const now = performance.now();
328
+ let dt = (now - lastFrameTimeRef.current) / 1000;
329
+ if (dt > 0.05)
330
+ dt = 0.05;
331
+ if (dt <= 0)
332
+ dt = 1 / 60;
333
+ lastFrameTimeRef.current = now;
334
+ // Sub-step the integrator for stability with stiff springs
335
+ const subSteps = 4;
336
+ const subDt = dt / subSteps;
337
+ for (let i = 0; i < subSteps; i++) {
338
+ if (s.hasX) {
339
+ const force = -s.stiffness * (obj.position.x - s.targetX) - s.damping * s.velX;
340
+ s.velX += force * subDt;
341
+ obj.position.x += s.velX * subDt;
342
+ }
343
+ if (s.hasY) {
344
+ const force = -s.stiffness * (obj.position.y - s.targetY) - s.damping * s.velY;
345
+ s.velY += force * subDt;
346
+ obj.position.y += s.velY * subDt;
347
+ }
348
+ if (s.hasZ) {
349
+ const force = -s.stiffness * (obj.position.z - s.targetZ) - s.damping * s.velZ;
350
+ s.velZ += force * subDt;
351
+ obj.position.z += s.velZ * subDt;
352
+ }
353
+ }
354
+ // Rest detection — stop when both speed and offset are tiny
355
+ const speedSq = s.velX * s.velX + s.velY * s.velY + s.velZ * s.velZ;
356
+ const offX = s.hasX ? obj.position.x - s.targetX : 0;
357
+ const offY = s.hasY ? obj.position.y - s.targetY : 0;
358
+ const offZ = s.hasZ ? obj.position.z - s.targetZ : 0;
359
+ const offsetSq = offX * offX + offY * offY + offZ * offZ;
360
+ if (speedSq < 0.0005 && offsetSq < 0.0005) {
361
+ if (s.hasX)
362
+ obj.position.x = s.targetX;
363
+ if (s.hasY)
364
+ obj.position.y = s.targetY;
365
+ if (s.hasZ)
366
+ obj.position.z = s.targetZ;
367
+ s.active = false;
368
+ (_a = s.onComplete) === null || _a === void 0 ? void 0 : _a.call(s);
369
+ }
370
+ });
371
+ const handlePointerDown = react.useCallback((event) => {
372
+ const instance = instanceRef.current;
373
+ if (!instance)
374
+ return;
375
+ event.stopPropagation();
376
+ // Halt any in-flight motion-library animation (e.g. main `animate` prop
377
+ // tween that's still running) and any active drag spring.
378
+ stopAnimation();
379
+ if (springStateRef.current)
380
+ springStateRef.current.active = false;
381
+ if (whileDrag) {
382
+ preDragStateRef.current = captureInstanceState();
383
+ const targetValues = typeof whileDrag === "string"
384
+ ? resolveVariant(whileDrag)
385
+ : whileDrag;
386
+ animateToTarget(targetValues, transition || { duration: 0.1 });
387
+ }
388
+ const obj = instance;
389
+ // Capture initial pointer + position. All subsequent moves are computed
390
+ // as `initialPos + pixelDelta * worldPerPixel` — never re-derived from
391
+ // the live (potentially-mid-animation) instance position.
392
+ dragStartPointerRef.current.x = event.nativeEvent.clientX;
393
+ dragStartPointerRef.current.y = event.nativeEvent.clientY;
394
+ dragStartPosRef.current.set(obj.position.x, obj.position.y, obj.position.z);
395
+ targetPosRef.current.copy(dragStartPosRef.current);
396
+ lastPosRef.current.copy(dragStartPosRef.current);
397
+ lastDeltaRef.current.set(0, 0, 0);
398
+ velocityRef.current.set(0, 0, 0);
399
+ lastTimeRef.current = performance.now();
400
+ // Compute world-units-per-pixel at the object's perpendicular depth
401
+ // (distance from camera along its forward axis).
402
+ const cameraForward = new THREE__namespace.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
403
+ const cameraToObj = new THREE__namespace.Vector3().subVectors(dragStartPosRef.current, camera.position);
404
+ const depth = Math.abs(cameraToObj.dot(cameraForward));
405
+ const rect = gl.domElement.getBoundingClientRect();
406
+ worldPerPixelRef.current = getWorldPerPixel(camera, depth, rect.height);
407
+ // Cache camera basis vectors so screen X/Y maps correctly to world
408
+ // X/Y/Z even if the camera is tilted.
409
+ cameraRightRef.current.set(1, 0, 0).applyQuaternion(camera.quaternion);
410
+ cameraUpRef.current.set(0, 1, 0).applyQuaternion(camera.quaternion);
411
+ isDraggingRef.current = true;
412
+ pointerIdRef.current = event.nativeEvent.pointerId;
413
+ try {
414
+ gl.domElement.setPointerCapture(event.nativeEvent.pointerId);
415
+ }
416
+ catch (_a) {
417
+ // not all environments support setPointerCapture
418
+ }
419
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(event.nativeEvent, {
420
+ point: { x: event.nativeEvent.clientX, y: event.nativeEvent.clientY },
421
+ offset: { x: 0, y: 0, z: 0 },
422
+ delta: { x: 0, y: 0, z: 0 },
423
+ velocity: { x: 0, y: 0, z: 0 },
424
+ });
425
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
426
+ },
427
+ // eslint-disable-next-line react-hooks/exhaustive-deps
428
+ [
429
+ whileDrag,
430
+ onDragStart,
431
+ onPointerDown,
432
+ camera,
433
+ gl,
434
+ captureInstanceState,
435
+ resolveVariant,
436
+ animateToTarget,
437
+ stopAnimation,
438
+ transition,
439
+ instanceRef,
440
+ ]);
441
+ react.useEffect(() => {
442
+ const handlePointerMove = (event) => {
443
+ var _a;
444
+ if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
445
+ return;
446
+ const instance = instanceRef.current;
447
+ if (!instance)
448
+ return;
449
+ const cur = propsRef.current;
450
+ const dragAxis = cur.drag;
451
+ const elasticVal = typeof cur.dragElastic === "number"
452
+ ? cur.dragElastic
453
+ : cur.dragElastic === false
454
+ ? 0
455
+ : 0.5;
456
+ const constraints = cur.dragConstraints;
457
+ const pixelDx = event.clientX - dragStartPointerRef.current.x;
458
+ const pixelDy = event.clientY - dragStartPointerRef.current.y;
459
+ const scale = worldPerPixelRef.current;
460
+ let newX = dragStartPosRef.current.x;
461
+ let newY = dragStartPosRef.current.y;
462
+ let newZ = dragStartPosRef.current.z;
463
+ if (dragAxis === "z") {
464
+ // Vertical screen movement → Z. Cursor down (pixelDy > 0) = closer.
465
+ newZ = dragStartPosRef.current.z + pixelDy * scale;
466
+ }
467
+ else {
468
+ // Map screen XY to camera-relative world delta.
469
+ const r = cameraRightRef.current;
470
+ const u = cameraUpRef.current;
471
+ const worldDx = r.x * pixelDx * scale + u.x * -pixelDy * scale;
472
+ const worldDy = r.y * pixelDx * scale + u.y * -pixelDy * scale;
473
+ const worldDz = r.z * pixelDx * scale + u.z * -pixelDy * scale;
474
+ if (dragAxis === "x") {
475
+ newX = dragStartPosRef.current.x + worldDx;
476
+ }
477
+ else if (dragAxis === "y") {
478
+ newY = dragStartPosRef.current.y + worldDy;
479
+ }
480
+ else {
481
+ newX = dragStartPosRef.current.x + worldDx;
482
+ newY = dragStartPosRef.current.y + worldDy;
483
+ newZ = dragStartPosRef.current.z + worldDz;
484
+ }
485
+ }
486
+ // Constraints with elastic rubber-banding
487
+ if (constraints) {
488
+ if (dragAxis !== "y" && dragAxis !== "z") {
489
+ newX = clampWithElastic(newX, constraints.left, constraints.right, elasticVal);
490
+ }
491
+ if (dragAxis !== "x" && dragAxis !== "z") {
492
+ newY = clampWithElastic(newY, constraints.bottom, constraints.top, elasticVal);
493
+ }
494
+ }
495
+ // Velocity tracking (used for momentum on release)
496
+ const now = performance.now();
497
+ const dt = (now - lastTimeRef.current) / 1000;
498
+ if (dt > 0) {
499
+ const dx = newX - lastPosRef.current.x;
500
+ const dy = newY - lastPosRef.current.y;
501
+ const dz = newZ - lastPosRef.current.z;
502
+ velocityRef.current.set(dx / dt, dy / dt, dz / dt);
503
+ lastDeltaRef.current.set(dx, dy, dz);
504
+ }
505
+ lastTimeRef.current = now;
506
+ lastPosRef.current.set(newX, newY, newZ);
507
+ // Stash target — useFrame applies it next R3F frame.
508
+ targetPosRef.current.set(newX, newY, newZ);
509
+ const offset = {
510
+ x: newX - dragStartPosRef.current.x,
511
+ y: newY - dragStartPosRef.current.y,
512
+ z: newZ - dragStartPosRef.current.z,
513
+ };
514
+ (_a = cur.onDrag) === null || _a === void 0 ? void 0 : _a.call(cur, event, {
515
+ point: { x: event.clientX, y: event.clientY },
516
+ offset,
517
+ delta: {
518
+ x: lastDeltaRef.current.x,
519
+ y: lastDeltaRef.current.y,
520
+ z: lastDeltaRef.current.z,
521
+ },
522
+ velocity: {
523
+ x: velocityRef.current.x,
524
+ y: velocityRef.current.y,
525
+ z: velocityRef.current.z,
526
+ },
527
+ });
528
+ };
529
+ const handlePointerUp = (event) => {
530
+ var _a, _b;
531
+ if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
532
+ return;
533
+ isDraggingRef.current = false;
534
+ pointerIdRef.current = null;
535
+ const instance = instanceRef.current;
536
+ const cur = propsRef.current;
537
+ // Lock in the final dragged position synchronously. Once useFrame stops
538
+ // applying the drag target, a still-pending write from the *previous*
539
+ // momentum animation can land before the new animateToTarget() call
540
+ // reads instance.position to use as its "from" value — which causes the
541
+ // new animation to start from the old momentum's path instead of where
542
+ // the user actually released.
543
+ if (instance) {
544
+ const obj = instance;
545
+ const t = targetPosRef.current;
546
+ obj.position.set(t.x, t.y, t.z);
547
+ }
548
+ // Restore whileDrag visual state
549
+ if (cur.whileDrag && preDragStateRef.current) {
550
+ const targetValues = buildTargetFromState(preDragStateRef.current);
551
+ animateToTarget(targetValues, options.transition || { duration: 0.2 });
552
+ const dur = ((_a = options.transition) === null || _a === void 0 ? void 0 : _a.duration) || 0.2;
553
+ setTimeout(() => {
554
+ preDragStateRef.current = null;
555
+ }, dur * 1000);
556
+ }
557
+ if (!instance)
558
+ return;
559
+ const finalPos = targetPosRef.current;
560
+ const offset = {
561
+ x: finalPos.x - dragStartPosRef.current.x,
562
+ y: finalPos.y - dragStartPosRef.current.y,
563
+ z: finalPos.z - dragStartPosRef.current.z,
564
+ };
565
+ const vel = velocityRef.current;
566
+ // Read spring tuning from dragTransition (if user provided one).
567
+ const dt = cur.dragTransition;
568
+ const userStiffness = typeof (dt === null || dt === void 0 ? void 0 : dt.stiffness) === "number" ? dt.stiffness : undefined;
569
+ const userDamping = typeof (dt === null || dt === void 0 ? void 0 : dt.damping) === "number" ? dt.damping : undefined;
570
+ if (cur.dragSnapToOrigin) {
571
+ lastFrameTimeRef.current = performance.now();
572
+ springStateRef.current = {
573
+ active: true,
574
+ targetX: dragStartPosRef.current.x,
575
+ targetY: dragStartPosRef.current.y,
576
+ targetZ: dragStartPosRef.current.z,
577
+ velX: vel.x,
578
+ velY: vel.y,
579
+ velZ: vel.z,
580
+ hasX: true,
581
+ hasY: true,
582
+ hasZ: true,
583
+ stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 400,
584
+ damping: userDamping !== null && userDamping !== void 0 ? userDamping : 40,
585
+ };
586
+ }
587
+ else if (cur.dragMomentum !== false) {
588
+ const speed = Math.sqrt(Math.pow(vel.x, 2) + Math.pow(vel.y, 2) + Math.pow(vel.z, 2));
589
+ if (speed > 0.5) {
590
+ const coeff = 0.25;
591
+ const dragAxis = cur.drag;
592
+ const hasX = dragAxis !== "y" && dragAxis !== "z";
593
+ const hasY = dragAxis !== "x" && dragAxis !== "z";
594
+ const hasZ = dragAxis === "z";
595
+ let targetX = hasX ? finalPos.x + vel.x * coeff : finalPos.x;
596
+ let targetY = hasY ? finalPos.y + vel.y * coeff : finalPos.y;
597
+ const targetZ = hasZ ? finalPos.z + vel.z * coeff : finalPos.z;
598
+ const constraints = cur.dragConstraints;
599
+ if (constraints) {
600
+ if (hasX) {
601
+ if (constraints.left !== undefined)
602
+ targetX = Math.max(targetX, constraints.left);
603
+ if (constraints.right !== undefined)
604
+ targetX = Math.min(targetX, constraints.right);
605
+ }
606
+ if (hasY) {
607
+ if (constraints.bottom !== undefined)
608
+ targetY = Math.max(targetY, constraints.bottom);
609
+ if (constraints.top !== undefined)
610
+ targetY = Math.min(targetY, constraints.top);
611
+ }
612
+ }
613
+ lastFrameTimeRef.current = performance.now();
614
+ springStateRef.current = {
615
+ active: true,
616
+ targetX,
617
+ targetY,
618
+ targetZ,
619
+ velX: hasX ? vel.x : 0,
620
+ velY: hasY ? vel.y : 0,
621
+ velZ: hasZ ? vel.z : 0,
622
+ hasX,
623
+ hasY,
624
+ hasZ,
625
+ stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 200,
626
+ damping: userDamping !== null && userDamping !== void 0 ? userDamping : 50,
627
+ };
628
+ }
629
+ }
630
+ (_b = cur.onDragEnd) === null || _b === void 0 ? void 0 : _b.call(cur, event, {
631
+ point: { x: event.clientX, y: event.clientY },
632
+ offset,
633
+ delta: {
634
+ x: lastDeltaRef.current.x,
635
+ y: lastDeltaRef.current.y,
636
+ z: lastDeltaRef.current.z,
637
+ },
638
+ velocity: { x: vel.x, y: vel.y, z: vel.z },
639
+ });
640
+ };
641
+ window.addEventListener("pointermove", handlePointerMove);
642
+ window.addEventListener("pointerup", handlePointerUp);
643
+ return () => {
644
+ window.removeEventListener("pointermove", handlePointerMove);
645
+ window.removeEventListener("pointerup", handlePointerUp);
646
+ };
647
+ // eslint-disable-next-line react-hooks/exhaustive-deps
648
+ }, [gl, instanceRef, animateToTarget, buildTargetFromState]);
649
+ const isDragEnabled = drag !== undefined && drag !== false;
650
+ if (!isDragEnabled)
651
+ return {};
652
+ return { onPointerDown: handlePointerDown };
653
+ }
654
+
252
655
  function createAnimationState() {
253
656
  return {
254
657
  hasStarted: false,
@@ -533,14 +936,20 @@ function custom(Component) {
533
936
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
534
937
  // eslint-disable-next-line react-hooks/exhaustive-deps
535
938
  }, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
939
+ const stopAnimation = react.useCallback(() => {
940
+ var _a;
941
+ (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop();
942
+ }, []);
536
943
  const gestureProps = {
944
+ instanceRef,
537
945
  captureInstanceState,
538
946
  buildTargetFromState,
539
947
  animateToTarget,
540
948
  resolveVariant,
541
949
  transition,
950
+ stopAnimation,
542
951
  };
543
- const gestureHandlers = Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps));
952
+ const gestureHandlers = Object.assign(Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps)), useDrag(false, props, gestureProps));
544
953
  const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
545
954
  const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
546
955
  const getNextChildIndex = react.useCallback(() => {
@@ -581,11 +990,11 @@ const MotionCamera = (props) => {
581
990
  const aspect = size.width / size.height;
582
991
  let cam;
583
992
  if (type === "perspective") {
584
- cam = new three.PerspectiveCamera(fov, aspect, near, far);
993
+ cam = new THREE.PerspectiveCamera(fov, aspect, near, far);
585
994
  }
586
995
  else {
587
996
  const frustumSize = 10;
588
- cam = new three.OrthographicCamera((-frustumSize * aspect) / 2, (frustumSize * aspect) / 2, frustumSize / 2, -frustumSize / 2, near, far);
997
+ cam = new THREE.OrthographicCamera((-frustumSize * aspect) / 2, (frustumSize * aspect) / 2, frustumSize / 2, -frustumSize / 2, near, far);
589
998
  }
590
999
  cam.updateProjectionMatrix();
591
1000
  return cam;
@@ -710,7 +1119,146 @@ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
710
1119
  });
711
1120
  }
712
1121
 
1122
+ const DEFAULT_SPRING = {
1123
+ type: 'spring',
1124
+ stiffness: 600,
1125
+ damping: 100,
1126
+ restDelta: 0.001,
1127
+ };
1128
+ // Seconds of velocity to project past the release point when snapping —
1129
+ // higher value = a flick advances further.
1130
+ const VELOCITY_PROJECTION = 0.2;
1131
+ // Fraction of slideWidth the user must cross, or minimum velocity (world
1132
+ // units/sec) required, to commit to the next slide. Below both thresholds
1133
+ // the carousel snaps back to the slot the drag started on.
1134
+ const DRAG_THRESHOLD_RATIO = 0.25;
1135
+ const FLICK_VELOCITY = 1.0;
1136
+ const mod = (n, m) => ((n % m) + m) % m;
1137
+ const CarouselSlot = react.memo(({ hadDragRef, item, itemWidth, slotRef }) => {
1138
+ const contentRef = react.useRef(null);
1139
+ const coverRef = react.useRef(null);
1140
+ react.useLayoutEffect(() => {
1141
+ const content = contentRef.current;
1142
+ const cover = coverRef.current;
1143
+ if (!content || !cover)
1144
+ return;
1145
+ const box = new THREE.Box3().setFromObject(content);
1146
+ if (box.isEmpty())
1147
+ return;
1148
+ const h = box.max.y - box.min.y;
1149
+ cover.geometry.dispose();
1150
+ cover.geometry = new THREE.PlaneGeometry(itemWidth, h);
1151
+ cover.position.z = box.max.z + 0.001;
1152
+ }, [item, itemWidth]);
1153
+ // Dispose the generated geometry on unmount — useLayoutEffect's replacement
1154
+ // logic only disposes the *previous* geometry, not the final one.
1155
+ react.useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
1156
+ return (jsxRuntime.jsxs("group", { ref: slotRef, children: [jsxRuntime.jsx("group", { ref: contentRef, children: item }), jsxRuntime.jsxs("mesh", { ref: coverRef, onClick: (e) => {
1157
+ if (hadDragRef.current) {
1158
+ e.stopPropagation();
1159
+ hadDragRef.current = false;
1160
+ }
1161
+ }, children: [jsxRuntime.jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsxRuntime.jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
1162
+ });
1163
+ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold = 1, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY, }) => {
1164
+ const slideWidth = itemWidth + gap;
1165
+ const count = items.length;
1166
+ // Render twice the items and center on the first item of the second copy.
1167
+ // The first copy lives to the left, so wrap-points sit at ±N*slideWidth
1168
+ // from camera center — well off-screen — instead of ±N*slideWidth/2
1169
+ // where they'd pop in and out at the visible edges.
1170
+ const loopedItems = react.useMemo(() => [...items, ...items], [items]);
1171
+ const slotCount = loopedItems.length;
1172
+ const period = slotCount * slideWidth;
1173
+ const startSlot = count + defaultValue;
1174
+ const groupRef = react.useRef(null);
1175
+ const itemRefs = react.useRef([]);
1176
+ // Single source of truth: a fractional slot index that grows up/down
1177
+ // infinitely as the user navigates. group.position.x = -currIndex * slideWidth
1178
+ // at rest. Snap animations animate currIndex toward integer targets;
1179
+ // useFrame applies it to the group every frame (when not dragging).
1180
+ const currIndex = react$1.useMotionValue(startSlot);
1181
+ const isDraggingRef = react.useRef(false);
1182
+ const hadDragRef = react.useRef(false);
1183
+ const dragStartIndexRef = react.useRef(startSlot);
1184
+ const snapAnimRef = react.useRef(null);
1185
+ const initial = react.useMemo(() => ({ x: -startSlot * slideWidth }),
1186
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1187
+ []);
1188
+ const goTo = react.useCallback((slot) => {
1189
+ var _a;
1190
+ onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
1191
+ (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
1192
+ snapAnimRef.current = react$1.animate(currIndex, slot, transition);
1193
+ }, [count, transition, onSwitch, currIndex]);
1194
+ react.useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
1195
+ // Drag start: stop any in-flight snap and capture the slot we started on
1196
+ // so an under-threshold release can elastic back to it.
1197
+ const handleDragStart = react.useCallback(() => {
1198
+ var _a;
1199
+ isDraggingRef.current = true;
1200
+ hadDragRef.current = false;
1201
+ dragStartIndexRef.current = Math.round(currIndex.get());
1202
+ (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
1203
+ snapAnimRef.current = null;
1204
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart();
1205
+ }, [currIndex, onDragStart]);
1206
+ const handleDragEnd = react.useCallback((_, info) => {
1207
+ isDraggingRef.current = false;
1208
+ const group = groupRef.current;
1209
+ if (!group)
1210
+ return;
1211
+ // Resync the motion value to where use-drag actually locked the position
1212
+ // — the next useFrame will use this same value, so there's no visible
1213
+ // jump between the dragged position and the start of the snap tween.
1214
+ const releasedX = group.position.x;
1215
+ const releasedIndex = -releasedX / slideWidth;
1216
+ currIndex.jump(releasedIndex);
1217
+ const startIndex = dragStartIndexRef.current;
1218
+ const offsetIndex = releasedIndex - startIndex;
1219
+ const overThreshold = Math.abs(offsetIndex) > dragThreshold;
1220
+ const overVelocity = Math.abs(info.velocity.x) > flickVelocity;
1221
+ if (!overThreshold && !overVelocity) {
1222
+ goTo(startIndex);
1223
+ return;
1224
+ }
1225
+ hadDragRef.current = true;
1226
+ const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
1227
+ const targetSlot = Math.round(-projectedX / slideWidth);
1228
+ goTo(targetSlot);
1229
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(info);
1230
+ }, [slideWidth, goTo, currIndex, dragThreshold, flickVelocity, onDragEnd]);
1231
+ // Drive position from currIndex unless the user is dragging — during drag
1232
+ // use-drag writes to group.position.x directly (and dragMomentum=false
1233
+ // ensures it stops writing the moment the pointer lifts, so there's no
1234
+ // race with the snap animation).
1235
+ fiber.useFrame(() => {
1236
+ const group = groupRef.current;
1237
+ if (!group)
1238
+ return;
1239
+ if (!isDraggingRef.current) {
1240
+ group.position.x = -currIndex.get() * slideWidth;
1241
+ }
1242
+ const groupX = group.position.x;
1243
+ for (let i = 0; i < slotCount; i++) {
1244
+ const ref = itemRefs.current[i];
1245
+ if (!ref)
1246
+ continue;
1247
+ const k = Math.round((-groupX - i * slideWidth) / period);
1248
+ ref.position.x = i * slideWidth + k * period;
1249
+ // +0.5 margin so the leading-edge slot fades in the moment the trailing
1250
+ // one fades out — keeps the loop seamless mid-drag instead of waiting
1251
+ // for snap to settle on an integer slot.
1252
+ const slotDist = (groupX + ref.position.x) / slideWidth;
1253
+ ref.visible = Math.abs(slotDist) <= renderThreshold + 0.5;
1254
+ }
1255
+ });
1256
+ return (jsxRuntime.jsx(motion.group, { ref: groupRef, drag: "x", dragMomentum: false, initial: initial, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => (jsxRuntime.jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, hadDragRef: hadDragRef }, `carouselItem-${i}`))) }));
1257
+ };
1258
+ var index = react.memo(Carousel);
1259
+
713
1260
  exports.AnimatePresence = AnimatePresence;
1261
+ exports.Carousel = index;
714
1262
  exports.MotionCamera = MotionCamera;
715
1263
  exports.motion = motion;
716
1264
  exports.usePresence = usePresence;
@@ -0,0 +1,147 @@
1
+ "use client";
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { memo, useMemo, useRef, useCallback, useEffect, useLayoutEffect } from 'react';
4
+ import { useFrame } from '@react-three/fiber';
5
+ import { Box3, PlaneGeometry } from 'three';
6
+ import { useMotionValue, animate } from 'motion/react';
7
+ import { motion } from '../../render/motion.mjs';
8
+
9
+ const DEFAULT_SPRING = {
10
+ type: 'spring',
11
+ stiffness: 600,
12
+ damping: 100,
13
+ restDelta: 0.001,
14
+ };
15
+ // Seconds of velocity to project past the release point when snapping —
16
+ // higher value = a flick advances further.
17
+ const VELOCITY_PROJECTION = 0.2;
18
+ // Fraction of slideWidth the user must cross, or minimum velocity (world
19
+ // units/sec) required, to commit to the next slide. Below both thresholds
20
+ // the carousel snaps back to the slot the drag started on.
21
+ const DRAG_THRESHOLD_RATIO = 0.25;
22
+ const FLICK_VELOCITY = 1.0;
23
+ const mod = (n, m) => ((n % m) + m) % m;
24
+ const CarouselSlot = memo(({ hadDragRef, item, itemWidth, slotRef }) => {
25
+ const contentRef = useRef(null);
26
+ const coverRef = useRef(null);
27
+ useLayoutEffect(() => {
28
+ const content = contentRef.current;
29
+ const cover = coverRef.current;
30
+ if (!content || !cover)
31
+ return;
32
+ const box = new Box3().setFromObject(content);
33
+ if (box.isEmpty())
34
+ return;
35
+ const h = box.max.y - box.min.y;
36
+ cover.geometry.dispose();
37
+ cover.geometry = new PlaneGeometry(itemWidth, h);
38
+ cover.position.z = box.max.z + 0.001;
39
+ }, [item, itemWidth]);
40
+ // Dispose the generated geometry on unmount — useLayoutEffect's replacement
41
+ // logic only disposes the *previous* geometry, not the final one.
42
+ useEffect(() => () => { var _a; (_a = coverRef.current) === null || _a === void 0 ? void 0 : _a.geometry.dispose(); }, []);
43
+ return (jsxs("group", { ref: slotRef, children: [jsx("group", { ref: contentRef, children: item }), jsxs("mesh", { ref: coverRef, onClick: (e) => {
44
+ if (hadDragRef.current) {
45
+ e.stopPropagation();
46
+ hadDragRef.current = false;
47
+ }
48
+ }, children: [jsx("planeGeometry", { args: [itemWidth, itemWidth] }), jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })] })] }));
49
+ });
50
+ const Carousel = ({ items, itemWidth = 1.5, gap = 0.5, defaultValue = 0, transition = DEFAULT_SPRING, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold = 1, dragThreshold = DRAG_THRESHOLD_RATIO, flickVelocity = FLICK_VELOCITY, }) => {
51
+ const slideWidth = itemWidth + gap;
52
+ const count = items.length;
53
+ // Render twice the items and center on the first item of the second copy.
54
+ // The first copy lives to the left, so wrap-points sit at ±N*slideWidth
55
+ // from camera center — well off-screen — instead of ±N*slideWidth/2
56
+ // where they'd pop in and out at the visible edges.
57
+ const loopedItems = useMemo(() => [...items, ...items], [items]);
58
+ const slotCount = loopedItems.length;
59
+ const period = slotCount * slideWidth;
60
+ const startSlot = count + defaultValue;
61
+ const groupRef = useRef(null);
62
+ const itemRefs = useRef([]);
63
+ // Single source of truth: a fractional slot index that grows up/down
64
+ // infinitely as the user navigates. group.position.x = -currIndex * slideWidth
65
+ // at rest. Snap animations animate currIndex toward integer targets;
66
+ // useFrame applies it to the group every frame (when not dragging).
67
+ const currIndex = useMotionValue(startSlot);
68
+ const isDraggingRef = useRef(false);
69
+ const hadDragRef = useRef(false);
70
+ const dragStartIndexRef = useRef(startSlot);
71
+ const snapAnimRef = useRef(null);
72
+ const initial = useMemo(() => ({ x: -startSlot * slideWidth }),
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ []);
75
+ const goTo = useCallback((slot) => {
76
+ var _a;
77
+ onSwitch === null || onSwitch === void 0 ? void 0 : onSwitch(mod(slot, count));
78
+ (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
79
+ snapAnimRef.current = animate(currIndex, slot, transition);
80
+ }, [count, transition, onSwitch, currIndex]);
81
+ useEffect(() => () => { var _a; return (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop(); }, []);
82
+ // Drag start: stop any in-flight snap and capture the slot we started on
83
+ // so an under-threshold release can elastic back to it.
84
+ const handleDragStart = useCallback(() => {
85
+ var _a;
86
+ isDraggingRef.current = true;
87
+ hadDragRef.current = false;
88
+ dragStartIndexRef.current = Math.round(currIndex.get());
89
+ (_a = snapAnimRef.current) === null || _a === void 0 ? void 0 : _a.stop();
90
+ snapAnimRef.current = null;
91
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart();
92
+ }, [currIndex, onDragStart]);
93
+ const handleDragEnd = useCallback((_, info) => {
94
+ isDraggingRef.current = false;
95
+ const group = groupRef.current;
96
+ if (!group)
97
+ return;
98
+ // Resync the motion value to where use-drag actually locked the position
99
+ // — the next useFrame will use this same value, so there's no visible
100
+ // jump between the dragged position and the start of the snap tween.
101
+ const releasedX = group.position.x;
102
+ const releasedIndex = -releasedX / slideWidth;
103
+ currIndex.jump(releasedIndex);
104
+ const startIndex = dragStartIndexRef.current;
105
+ const offsetIndex = releasedIndex - startIndex;
106
+ const overThreshold = Math.abs(offsetIndex) > dragThreshold;
107
+ const overVelocity = Math.abs(info.velocity.x) > flickVelocity;
108
+ if (!overThreshold && !overVelocity) {
109
+ goTo(startIndex);
110
+ return;
111
+ }
112
+ hadDragRef.current = true;
113
+ const projectedX = releasedX + info.velocity.x * VELOCITY_PROJECTION;
114
+ const targetSlot = Math.round(-projectedX / slideWidth);
115
+ goTo(targetSlot);
116
+ onDragEnd === null || onDragEnd === void 0 ? void 0 : onDragEnd(info);
117
+ }, [slideWidth, goTo, currIndex, dragThreshold, flickVelocity, onDragEnd]);
118
+ // Drive position from currIndex unless the user is dragging — during drag
119
+ // use-drag writes to group.position.x directly (and dragMomentum=false
120
+ // ensures it stops writing the moment the pointer lifts, so there's no
121
+ // race with the snap animation).
122
+ useFrame(() => {
123
+ const group = groupRef.current;
124
+ if (!group)
125
+ return;
126
+ if (!isDraggingRef.current) {
127
+ group.position.x = -currIndex.get() * slideWidth;
128
+ }
129
+ const groupX = group.position.x;
130
+ for (let i = 0; i < slotCount; i++) {
131
+ const ref = itemRefs.current[i];
132
+ if (!ref)
133
+ continue;
134
+ const k = Math.round((-groupX - i * slideWidth) / period);
135
+ ref.position.x = i * slideWidth + k * period;
136
+ // +0.5 margin so the leading-edge slot fades in the moment the trailing
137
+ // one fades out — keeps the loop seamless mid-drag instead of waiting
138
+ // for snap to settle on an integer slot.
139
+ const slotDist = (groupX + ref.position.x) / slideWidth;
140
+ ref.visible = Math.abs(slotDist) <= renderThreshold + 0.5;
141
+ }
142
+ });
143
+ return (jsx(motion.group, { ref: groupRef, drag: "x", dragMomentum: false, initial: initial, onDragStart: handleDragStart, onDragEnd: handleDragEnd, onDrag: (_e, info) => onDrag === null || onDrag === void 0 ? void 0 : onDrag(info), children: loopedItems.map((item, i) => (jsx(CarouselSlot, { item: item, itemWidth: itemWidth, slotRef: (el) => { itemRefs.current[i] = el; }, hadDragRef: hadDragRef }, `carouselItem-${i}`))) }));
144
+ };
145
+ var index = memo(Carousel);
146
+
147
+ export { index as default };
package/dist/es/index.mjs CHANGED
@@ -3,3 +3,4 @@ export { motion } from './render/motion.mjs';
3
3
  export { default as MotionCamera } from './components/MotionCamera.mjs';
4
4
  export { AnimatePresence } from './components/AnimatePresence/index.mjs';
5
5
  export { usePresence } from './components/AnimatePresence/PresenceContext.mjs';
6
+ export { default as Carousel } from './components/Carousel/index.mjs';
@@ -0,0 +1,389 @@
1
+ "use client";
2
+ import { useRef, useCallback, useEffect } from 'react';
3
+ import { useThree, useFrame } from '@react-three/fiber';
4
+ import * as THREE from 'three';
5
+
6
+ // World-space units per CSS pixel at the given depth from the camera.
7
+ const getWorldPerPixel = (camera, depth, viewportHeight) => {
8
+ var _a;
9
+ const ortho = camera;
10
+ if (ortho.isOrthographicCamera) {
11
+ return ((ortho.top - ortho.bottom) / (viewportHeight * (ortho.zoom || 1)));
12
+ }
13
+ const persp = camera;
14
+ const fov = ((_a = persp.fov) !== null && _a !== void 0 ? _a : 60) * (Math.PI / 180);
15
+ return (2 * Math.tan(fov / 2) * depth) / viewportHeight;
16
+ };
17
+ const clampWithElastic = (value, min, max, elastic) => {
18
+ if (min !== undefined && value < min)
19
+ return min + (value - min) * elastic;
20
+ if (max !== undefined && value > max)
21
+ return max + (value - max) * elastic;
22
+ return value;
23
+ };
24
+ function useDrag(isStatic, props, options) {
25
+ const { camera, gl } = useThree();
26
+ const { drag, whileDrag, onDragStart, onPointerDown, transition } = props;
27
+ const { instanceRef, captureInstanceState, buildTargetFromState, animateToTarget, resolveVariant, stopAnimation, } = options;
28
+ const isDraggingRef = useRef(false);
29
+ const dragStartPointerRef = useRef({ x: 0, y: 0 });
30
+ const dragStartPosRef = useRef(new THREE.Vector3());
31
+ const cameraRightRef = useRef(new THREE.Vector3());
32
+ const cameraUpRef = useRef(new THREE.Vector3());
33
+ const worldPerPixelRef = useRef(0);
34
+ const targetPosRef = useRef(new THREE.Vector3());
35
+ const lastPosRef = useRef(new THREE.Vector3());
36
+ const velocityRef = useRef(new THREE.Vector3());
37
+ const lastDeltaRef = useRef(new THREE.Vector3());
38
+ const lastTimeRef = useRef(0);
39
+ const preDragStateRef = useRef(null);
40
+ const pointerIdRef = useRef(null);
41
+ const springStateRef = useRef(null);
42
+ const lastFrameTimeRef = useRef(0);
43
+ const propsRef = useRef(props);
44
+ propsRef.current = props;
45
+ useFrame(() => {
46
+ var _a;
47
+ const instance = instanceRef.current;
48
+ if (!instance)
49
+ return;
50
+ const obj = instance;
51
+ // Phase 1: dragging — apply the drag target
52
+ if (isDraggingRef.current) {
53
+ const t = targetPosRef.current;
54
+ obj.position.set(t.x, t.y, t.z);
55
+ return;
56
+ }
57
+ // Phase 2: spring (momentum / snap-to-origin)
58
+ const s = springStateRef.current;
59
+ if (!s || !s.active)
60
+ return;
61
+ const now = performance.now();
62
+ let dt = (now - lastFrameTimeRef.current) / 1000;
63
+ if (dt > 0.05)
64
+ dt = 0.05;
65
+ if (dt <= 0)
66
+ dt = 1 / 60;
67
+ lastFrameTimeRef.current = now;
68
+ // Sub-step the integrator for stability with stiff springs
69
+ const subSteps = 4;
70
+ const subDt = dt / subSteps;
71
+ for (let i = 0; i < subSteps; i++) {
72
+ if (s.hasX) {
73
+ const force = -s.stiffness * (obj.position.x - s.targetX) - s.damping * s.velX;
74
+ s.velX += force * subDt;
75
+ obj.position.x += s.velX * subDt;
76
+ }
77
+ if (s.hasY) {
78
+ const force = -s.stiffness * (obj.position.y - s.targetY) - s.damping * s.velY;
79
+ s.velY += force * subDt;
80
+ obj.position.y += s.velY * subDt;
81
+ }
82
+ if (s.hasZ) {
83
+ const force = -s.stiffness * (obj.position.z - s.targetZ) - s.damping * s.velZ;
84
+ s.velZ += force * subDt;
85
+ obj.position.z += s.velZ * subDt;
86
+ }
87
+ }
88
+ // Rest detection — stop when both speed and offset are tiny
89
+ const speedSq = s.velX * s.velX + s.velY * s.velY + s.velZ * s.velZ;
90
+ const offX = s.hasX ? obj.position.x - s.targetX : 0;
91
+ const offY = s.hasY ? obj.position.y - s.targetY : 0;
92
+ const offZ = s.hasZ ? obj.position.z - s.targetZ : 0;
93
+ const offsetSq = offX * offX + offY * offY + offZ * offZ;
94
+ if (speedSq < 0.0005 && offsetSq < 0.0005) {
95
+ if (s.hasX)
96
+ obj.position.x = s.targetX;
97
+ if (s.hasY)
98
+ obj.position.y = s.targetY;
99
+ if (s.hasZ)
100
+ obj.position.z = s.targetZ;
101
+ s.active = false;
102
+ (_a = s.onComplete) === null || _a === void 0 ? void 0 : _a.call(s);
103
+ }
104
+ });
105
+ const handlePointerDown = useCallback((event) => {
106
+ const instance = instanceRef.current;
107
+ if (!instance)
108
+ return;
109
+ event.stopPropagation();
110
+ // Halt any in-flight motion-library animation (e.g. main `animate` prop
111
+ // tween that's still running) and any active drag spring.
112
+ stopAnimation();
113
+ if (springStateRef.current)
114
+ springStateRef.current.active = false;
115
+ if (whileDrag) {
116
+ preDragStateRef.current = captureInstanceState();
117
+ const targetValues = typeof whileDrag === "string"
118
+ ? resolveVariant(whileDrag)
119
+ : whileDrag;
120
+ animateToTarget(targetValues, transition || { duration: 0.1 });
121
+ }
122
+ const obj = instance;
123
+ // Capture initial pointer + position. All subsequent moves are computed
124
+ // as `initialPos + pixelDelta * worldPerPixel` — never re-derived from
125
+ // the live (potentially-mid-animation) instance position.
126
+ dragStartPointerRef.current.x = event.nativeEvent.clientX;
127
+ dragStartPointerRef.current.y = event.nativeEvent.clientY;
128
+ dragStartPosRef.current.set(obj.position.x, obj.position.y, obj.position.z);
129
+ targetPosRef.current.copy(dragStartPosRef.current);
130
+ lastPosRef.current.copy(dragStartPosRef.current);
131
+ lastDeltaRef.current.set(0, 0, 0);
132
+ velocityRef.current.set(0, 0, 0);
133
+ lastTimeRef.current = performance.now();
134
+ // Compute world-units-per-pixel at the object's perpendicular depth
135
+ // (distance from camera along its forward axis).
136
+ const cameraForward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
137
+ const cameraToObj = new THREE.Vector3().subVectors(dragStartPosRef.current, camera.position);
138
+ const depth = Math.abs(cameraToObj.dot(cameraForward));
139
+ const rect = gl.domElement.getBoundingClientRect();
140
+ worldPerPixelRef.current = getWorldPerPixel(camera, depth, rect.height);
141
+ // Cache camera basis vectors so screen X/Y maps correctly to world
142
+ // X/Y/Z even if the camera is tilted.
143
+ cameraRightRef.current.set(1, 0, 0).applyQuaternion(camera.quaternion);
144
+ cameraUpRef.current.set(0, 1, 0).applyQuaternion(camera.quaternion);
145
+ isDraggingRef.current = true;
146
+ pointerIdRef.current = event.nativeEvent.pointerId;
147
+ try {
148
+ gl.domElement.setPointerCapture(event.nativeEvent.pointerId);
149
+ }
150
+ catch (_a) {
151
+ // not all environments support setPointerCapture
152
+ }
153
+ onDragStart === null || onDragStart === void 0 ? void 0 : onDragStart(event.nativeEvent, {
154
+ point: { x: event.nativeEvent.clientX, y: event.nativeEvent.clientY },
155
+ offset: { x: 0, y: 0, z: 0 },
156
+ delta: { x: 0, y: 0, z: 0 },
157
+ velocity: { x: 0, y: 0, z: 0 },
158
+ });
159
+ onPointerDown === null || onPointerDown === void 0 ? void 0 : onPointerDown(event);
160
+ },
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps
162
+ [
163
+ whileDrag,
164
+ onDragStart,
165
+ onPointerDown,
166
+ camera,
167
+ gl,
168
+ captureInstanceState,
169
+ resolveVariant,
170
+ animateToTarget,
171
+ stopAnimation,
172
+ transition,
173
+ instanceRef,
174
+ ]);
175
+ useEffect(() => {
176
+ const handlePointerMove = (event) => {
177
+ var _a;
178
+ if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
179
+ return;
180
+ const instance = instanceRef.current;
181
+ if (!instance)
182
+ return;
183
+ const cur = propsRef.current;
184
+ const dragAxis = cur.drag;
185
+ const elasticVal = typeof cur.dragElastic === "number"
186
+ ? cur.dragElastic
187
+ : cur.dragElastic === false
188
+ ? 0
189
+ : 0.5;
190
+ const constraints = cur.dragConstraints;
191
+ const pixelDx = event.clientX - dragStartPointerRef.current.x;
192
+ const pixelDy = event.clientY - dragStartPointerRef.current.y;
193
+ const scale = worldPerPixelRef.current;
194
+ let newX = dragStartPosRef.current.x;
195
+ let newY = dragStartPosRef.current.y;
196
+ let newZ = dragStartPosRef.current.z;
197
+ if (dragAxis === "z") {
198
+ // Vertical screen movement → Z. Cursor down (pixelDy > 0) = closer.
199
+ newZ = dragStartPosRef.current.z + pixelDy * scale;
200
+ }
201
+ else {
202
+ // Map screen XY to camera-relative world delta.
203
+ const r = cameraRightRef.current;
204
+ const u = cameraUpRef.current;
205
+ const worldDx = r.x * pixelDx * scale + u.x * -pixelDy * scale;
206
+ const worldDy = r.y * pixelDx * scale + u.y * -pixelDy * scale;
207
+ const worldDz = r.z * pixelDx * scale + u.z * -pixelDy * scale;
208
+ if (dragAxis === "x") {
209
+ newX = dragStartPosRef.current.x + worldDx;
210
+ }
211
+ else if (dragAxis === "y") {
212
+ newY = dragStartPosRef.current.y + worldDy;
213
+ }
214
+ else {
215
+ newX = dragStartPosRef.current.x + worldDx;
216
+ newY = dragStartPosRef.current.y + worldDy;
217
+ newZ = dragStartPosRef.current.z + worldDz;
218
+ }
219
+ }
220
+ // Constraints with elastic rubber-banding
221
+ if (constraints) {
222
+ if (dragAxis !== "y" && dragAxis !== "z") {
223
+ newX = clampWithElastic(newX, constraints.left, constraints.right, elasticVal);
224
+ }
225
+ if (dragAxis !== "x" && dragAxis !== "z") {
226
+ newY = clampWithElastic(newY, constraints.bottom, constraints.top, elasticVal);
227
+ }
228
+ }
229
+ // Velocity tracking (used for momentum on release)
230
+ const now = performance.now();
231
+ const dt = (now - lastTimeRef.current) / 1000;
232
+ if (dt > 0) {
233
+ const dx = newX - lastPosRef.current.x;
234
+ const dy = newY - lastPosRef.current.y;
235
+ const dz = newZ - lastPosRef.current.z;
236
+ velocityRef.current.set(dx / dt, dy / dt, dz / dt);
237
+ lastDeltaRef.current.set(dx, dy, dz);
238
+ }
239
+ lastTimeRef.current = now;
240
+ lastPosRef.current.set(newX, newY, newZ);
241
+ // Stash target — useFrame applies it next R3F frame.
242
+ targetPosRef.current.set(newX, newY, newZ);
243
+ const offset = {
244
+ x: newX - dragStartPosRef.current.x,
245
+ y: newY - dragStartPosRef.current.y,
246
+ z: newZ - dragStartPosRef.current.z,
247
+ };
248
+ (_a = cur.onDrag) === null || _a === void 0 ? void 0 : _a.call(cur, event, {
249
+ point: { x: event.clientX, y: event.clientY },
250
+ offset,
251
+ delta: {
252
+ x: lastDeltaRef.current.x,
253
+ y: lastDeltaRef.current.y,
254
+ z: lastDeltaRef.current.z,
255
+ },
256
+ velocity: {
257
+ x: velocityRef.current.x,
258
+ y: velocityRef.current.y,
259
+ z: velocityRef.current.z,
260
+ },
261
+ });
262
+ };
263
+ const handlePointerUp = (event) => {
264
+ var _a, _b;
265
+ if (!isDraggingRef.current || event.pointerId !== pointerIdRef.current)
266
+ return;
267
+ isDraggingRef.current = false;
268
+ pointerIdRef.current = null;
269
+ const instance = instanceRef.current;
270
+ const cur = propsRef.current;
271
+ // Lock in the final dragged position synchronously. Once useFrame stops
272
+ // applying the drag target, a still-pending write from the *previous*
273
+ // momentum animation can land before the new animateToTarget() call
274
+ // reads instance.position to use as its "from" value — which causes the
275
+ // new animation to start from the old momentum's path instead of where
276
+ // the user actually released.
277
+ if (instance) {
278
+ const obj = instance;
279
+ const t = targetPosRef.current;
280
+ obj.position.set(t.x, t.y, t.z);
281
+ }
282
+ // Restore whileDrag visual state
283
+ if (cur.whileDrag && preDragStateRef.current) {
284
+ const targetValues = buildTargetFromState(preDragStateRef.current);
285
+ animateToTarget(targetValues, options.transition || { duration: 0.2 });
286
+ const dur = ((_a = options.transition) === null || _a === void 0 ? void 0 : _a.duration) || 0.2;
287
+ setTimeout(() => {
288
+ preDragStateRef.current = null;
289
+ }, dur * 1000);
290
+ }
291
+ if (!instance)
292
+ return;
293
+ const finalPos = targetPosRef.current;
294
+ const offset = {
295
+ x: finalPos.x - dragStartPosRef.current.x,
296
+ y: finalPos.y - dragStartPosRef.current.y,
297
+ z: finalPos.z - dragStartPosRef.current.z,
298
+ };
299
+ const vel = velocityRef.current;
300
+ // Read spring tuning from dragTransition (if user provided one).
301
+ const dt = cur.dragTransition;
302
+ const userStiffness = typeof (dt === null || dt === void 0 ? void 0 : dt.stiffness) === "number" ? dt.stiffness : undefined;
303
+ const userDamping = typeof (dt === null || dt === void 0 ? void 0 : dt.damping) === "number" ? dt.damping : undefined;
304
+ if (cur.dragSnapToOrigin) {
305
+ lastFrameTimeRef.current = performance.now();
306
+ springStateRef.current = {
307
+ active: true,
308
+ targetX: dragStartPosRef.current.x,
309
+ targetY: dragStartPosRef.current.y,
310
+ targetZ: dragStartPosRef.current.z,
311
+ velX: vel.x,
312
+ velY: vel.y,
313
+ velZ: vel.z,
314
+ hasX: true,
315
+ hasY: true,
316
+ hasZ: true,
317
+ stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 400,
318
+ damping: userDamping !== null && userDamping !== void 0 ? userDamping : 40,
319
+ };
320
+ }
321
+ else if (cur.dragMomentum !== false) {
322
+ const speed = Math.sqrt(Math.pow(vel.x, 2) + Math.pow(vel.y, 2) + Math.pow(vel.z, 2));
323
+ if (speed > 0.5) {
324
+ const coeff = 0.25;
325
+ const dragAxis = cur.drag;
326
+ const hasX = dragAxis !== "y" && dragAxis !== "z";
327
+ const hasY = dragAxis !== "x" && dragAxis !== "z";
328
+ const hasZ = dragAxis === "z";
329
+ let targetX = hasX ? finalPos.x + vel.x * coeff : finalPos.x;
330
+ let targetY = hasY ? finalPos.y + vel.y * coeff : finalPos.y;
331
+ const targetZ = hasZ ? finalPos.z + vel.z * coeff : finalPos.z;
332
+ const constraints = cur.dragConstraints;
333
+ if (constraints) {
334
+ if (hasX) {
335
+ if (constraints.left !== undefined)
336
+ targetX = Math.max(targetX, constraints.left);
337
+ if (constraints.right !== undefined)
338
+ targetX = Math.min(targetX, constraints.right);
339
+ }
340
+ if (hasY) {
341
+ if (constraints.bottom !== undefined)
342
+ targetY = Math.max(targetY, constraints.bottom);
343
+ if (constraints.top !== undefined)
344
+ targetY = Math.min(targetY, constraints.top);
345
+ }
346
+ }
347
+ lastFrameTimeRef.current = performance.now();
348
+ springStateRef.current = {
349
+ active: true,
350
+ targetX,
351
+ targetY,
352
+ targetZ,
353
+ velX: hasX ? vel.x : 0,
354
+ velY: hasY ? vel.y : 0,
355
+ velZ: hasZ ? vel.z : 0,
356
+ hasX,
357
+ hasY,
358
+ hasZ,
359
+ stiffness: userStiffness !== null && userStiffness !== void 0 ? userStiffness : 200,
360
+ damping: userDamping !== null && userDamping !== void 0 ? userDamping : 50,
361
+ };
362
+ }
363
+ }
364
+ (_b = cur.onDragEnd) === null || _b === void 0 ? void 0 : _b.call(cur, event, {
365
+ point: { x: event.clientX, y: event.clientY },
366
+ offset,
367
+ delta: {
368
+ x: lastDeltaRef.current.x,
369
+ y: lastDeltaRef.current.y,
370
+ z: lastDeltaRef.current.z,
371
+ },
372
+ velocity: { x: vel.x, y: vel.y, z: vel.z },
373
+ });
374
+ };
375
+ window.addEventListener("pointermove", handlePointerMove);
376
+ window.addEventListener("pointerup", handlePointerUp);
377
+ return () => {
378
+ window.removeEventListener("pointermove", handlePointerMove);
379
+ window.removeEventListener("pointerup", handlePointerUp);
380
+ };
381
+ // eslint-disable-next-line react-hooks/exhaustive-deps
382
+ }, [gl, instanceRef, animateToTarget, buildTargetFromState]);
383
+ const isDragEnabled = drag !== undefined && drag !== false;
384
+ if (!isDragEnabled)
385
+ return {};
386
+ return { onPointerDown: handlePointerDown };
387
+ }
388
+
389
+ export { useDrag };
@@ -6,6 +6,7 @@ import { PresenceContext } from '../components/AnimatePresence/PresenceContext.m
6
6
  import { useRender } from './use-render.mjs';
7
7
  import { useHover } from './gestures/use-hover.mjs';
8
8
  import { useTap } from './gestures/use-tap.mjs';
9
+ import { useDrag } from './gestures/use-drag.mjs';
9
10
  import { createAnimationState, createCallbackOptions, registerAnimation } from './events/index.mjs';
10
11
 
11
12
  const MotionContext = createContext(null);
@@ -245,14 +246,20 @@ function custom(Component) {
245
246
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
246
247
  // eslint-disable-next-line react-hooks/exhaustive-deps
247
248
  }, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
249
+ const stopAnimation = useCallback(() => {
250
+ var _a;
251
+ (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop();
252
+ }, []);
248
253
  const gestureProps = {
254
+ instanceRef,
249
255
  captureInstanceState,
250
256
  buildTargetFromState,
251
257
  animateToTarget,
252
258
  resolveVariant,
253
259
  transition,
260
+ stopAnimation,
254
261
  };
255
- const gestureHandlers = Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps));
262
+ const gestureHandlers = Object.assign(Object.assign(Object.assign({}, useHover(false, props, gestureProps)), useTap(false, props, gestureProps)), useDrag(false, props, gestureProps));
256
263
  const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
257
264
  const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
258
265
  const getNextChildIndex = useCallback(() => {
package/dist/index.d.ts CHANGED
@@ -2,12 +2,49 @@ import { Vector3, Euler, Color, ReactThreeFiber } from '@react-three/fiber';
2
2
  import * as THREE from 'three';
3
3
  import * as react from 'react';
4
4
  import { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, ReactNode } from 'react';
5
- import { MotionValue, MotionProps, ResolvedValues } from 'motion/react';
5
+ import { MotionValue, MotionProps, Transition, ResolvedValues } from 'motion/react';
6
6
  import * as react_jsx_runtime from 'react/jsx-runtime';
7
7
 
8
8
  type ThreeElement = InstanceType<typeof THREE.Object3D> & Record<string, unknown>;
9
- interface ThreeMotionProps extends Omit<MotionProps, "style" | "children" | "onUpdate" | "onAnimationStart" | "onAnimationComplete"> {
9
+ interface DragInfo {
10
+ point: {
11
+ x: number;
12
+ y: number;
13
+ };
14
+ offset: {
15
+ x: number;
16
+ y: number;
17
+ z: number;
18
+ };
19
+ delta: {
20
+ x: number;
21
+ y: number;
22
+ z: number;
23
+ };
24
+ velocity: {
25
+ x: number;
26
+ y: number;
27
+ z: number;
28
+ };
29
+ }
30
+ interface DragConstraints {
31
+ left?: number;
32
+ right?: number;
33
+ top?: number;
34
+ bottom?: number;
35
+ }
36
+ interface ThreeMotionProps extends Omit<MotionProps, "style" | "children" | "onUpdate" | "onAnimationStart" | "onAnimationComplete" | "drag" | "dragConstraints" | "dragElastic" | "dragMomentum" | "dragSnapToOrigin" | "dragTransition" | "onDrag" | "onDragStart" | "onDragEnd" | "whileDrag" | "dragDirectionLock" | "dragPropagation" | "dragListener"> {
10
37
  [key: string]: unknown;
38
+ drag?: boolean | "x" | "y" | "z";
39
+ dragConstraints?: DragConstraints;
40
+ dragElastic?: number | boolean;
41
+ dragMomentum?: boolean;
42
+ dragSnapToOrigin?: boolean;
43
+ dragTransition?: Transition;
44
+ whileDrag?: Record<string, unknown> | string;
45
+ onDragStart?: (event: PointerEvent, info: DragInfo) => void;
46
+ onDrag?: (event: PointerEvent, info: DragInfo) => void;
47
+ onDragEnd?: (event: PointerEvent, info: DragInfo) => void;
11
48
  onInstanceUpdate?: ReactThreeFiber.ThreeElements["object3D"]["onUpdate"];
12
49
  onUpdate?: (values: Record<string, unknown>, animationVariant?: string) => void;
13
50
  onAnimationStart?: (values: Record<string, unknown>, animationVariant?: string) => void;
@@ -90,5 +127,22 @@ interface AnimatePresenceProps {
90
127
  }
91
128
  declare function AnimatePresence({ children, mode, onExitComplete, custom, }: AnimatePresenceProps): react.FunctionComponentElement<react.ProviderProps<PresenceContextValue | null>>[];
92
129
 
93
- export { AnimatePresence, MotionCamera, motion, usePresence };
94
- export type { AcceptMotionValues, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
130
+ interface CarouselProps {
131
+ items: ReactNode[];
132
+ itemWidth?: number;
133
+ gap?: number;
134
+ defaultValue?: number;
135
+ transition?: Transition;
136
+ onSwitch?: (index: number) => void;
137
+ onDragStart?: () => void;
138
+ onDrag?: (info: DragInfo) => void;
139
+ onDragEnd?: (info: DragInfo) => void;
140
+ renderThreshold?: number;
141
+ dragThreshold?: number;
142
+ flickVelocity?: number;
143
+ }
144
+ declare const Carousel: ({ items, itemWidth, gap, defaultValue, transition, onSwitch, onDragStart, onDrag, onDragEnd, renderThreshold, dragThreshold, flickVelocity, }: CarouselProps) => react_jsx_runtime.JSX.Element;
145
+ declare const _default: typeof Carousel;
146
+
147
+ export { AnimatePresence, _default as Carousel, MotionCamera, motion, usePresence };
148
+ export type { AcceptMotionValues, CarouselProps, DragConstraints, DragInfo, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "r3f-motion",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "A simple and powerful React animation library for @react-three/fiber leveraging motion.dev",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/es/index.mjs",