r3f-motion 1.0.4 → 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
  /**
@@ -32,6 +52,7 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
32
52
  * Create a callback ref that captures the Three.js instance
33
53
  */
34
54
  const callbackRef = react.useCallback((instance) => {
55
+ var _a;
35
56
  if (!instance)
36
57
  return;
37
58
  // Apply initial values immediately to prevent FOUC - but only once
@@ -70,6 +91,12 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
70
91
  colorProp.set(initialValues[key]);
71
92
  }
72
93
  }
94
+ else if (instance.uniforms &&
95
+ instance.uniforms[key] &&
96
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
97
+ // Set ShaderMaterial uniform initial value
98
+ instance.uniforms[key].value = initialValues[key];
99
+ }
73
100
  else if (key in instance && typeof instance[key] === "number") {
74
101
  instance[key] = initialValues[key];
75
102
  }
@@ -242,6 +269,389 @@ function useTap(isStatic, props, options) {
242
269
  };
243
270
  }
244
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
+
245
655
  function createAnimationState() {
246
656
  return {
247
657
  hasStarted: false,
@@ -447,6 +857,7 @@ function custom(Component) {
447
857
  "attenuationColor",
448
858
  ]);
449
859
  Object.entries(targetValues).forEach(([key, value]) => {
860
+ var _a;
450
861
  const opts = getPropertyOpts(key);
451
862
  const mapping = transformMap[key];
452
863
  if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
@@ -460,6 +871,14 @@ function custom(Component) {
460
871
  else if (colorKeys.has(key) && instance[key]) {
461
872
  animateColor(instance[key], value, opts, key);
462
873
  }
874
+ else if (instance.uniforms &&
875
+ instance.uniforms[key] &&
876
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
877
+ // Animate ShaderMaterial uniforms (uniforms[key].value)
878
+ // Note: consumers must useMemo their uniforms prop to prevent
879
+ // R3F from overwriting animated values on re-render
880
+ createAnimation(instance.uniforms[key], { value: value }, opts, key);
881
+ }
463
882
  else if (key in instance && typeof instance[key] === "number") {
464
883
  createAnimation(instance, { [key]: value }, opts, key);
465
884
  }
@@ -517,14 +936,20 @@ function custom(Component) {
517
936
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
518
937
  // eslint-disable-next-line react-hooks/exhaustive-deps
519
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
+ }, []);
520
943
  const gestureProps = {
944
+ instanceRef,
521
945
  captureInstanceState,
522
946
  buildTargetFromState,
523
947
  animateToTarget,
524
948
  resolveVariant,
525
949
  transition,
950
+ stopAnimation,
526
951
  };
527
- 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));
528
953
  const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
529
954
  const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
530
955
  const getNextChildIndex = react.useCallback(() => {
@@ -565,11 +990,11 @@ const MotionCamera = (props) => {
565
990
  const aspect = size.width / size.height;
566
991
  let cam;
567
992
  if (type === "perspective") {
568
- cam = new three.PerspectiveCamera(fov, aspect, near, far);
993
+ cam = new THREE.PerspectiveCamera(fov, aspect, near, far);
569
994
  }
570
995
  else {
571
996
  const frustumSize = 10;
572
- 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);
573
998
  }
574
999
  cam.updateProjectionMatrix();
575
1000
  return cam;
@@ -694,7 +1119,146 @@ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
694
1119
  });
695
1120
  }
696
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
+
697
1260
  exports.AnimatePresence = AnimatePresence;
1261
+ exports.Carousel = index;
698
1262
  exports.MotionCamera = MotionCamera;
699
1263
  exports.motion = motion;
700
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);
@@ -166,6 +167,7 @@ function custom(Component) {
166
167
  "attenuationColor",
167
168
  ]);
168
169
  Object.entries(targetValues).forEach(([key, value]) => {
170
+ var _a;
169
171
  const opts = getPropertyOpts(key);
170
172
  const mapping = transformMap[key];
171
173
  if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
@@ -179,6 +181,14 @@ function custom(Component) {
179
181
  else if (colorKeys.has(key) && instance[key]) {
180
182
  animateColor(instance[key], value, opts, key);
181
183
  }
184
+ else if (instance.uniforms &&
185
+ instance.uniforms[key] &&
186
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
187
+ // Animate ShaderMaterial uniforms (uniforms[key].value)
188
+ // Note: consumers must useMemo their uniforms prop to prevent
189
+ // R3F from overwriting animated values on re-render
190
+ createAnimation(instance.uniforms[key], { value: value }, opts, key);
191
+ }
182
192
  else if (key in instance && typeof instance[key] === "number") {
183
193
  createAnimation(instance, { [key]: value }, opts, key);
184
194
  }
@@ -236,14 +246,20 @@ function custom(Component) {
236
246
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
237
247
  // eslint-disable-next-line react-hooks/exhaustive-deps
238
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
+ }, []);
239
253
  const gestureProps = {
254
+ instanceRef,
240
255
  captureInstanceState,
241
256
  buildTargetFromState,
242
257
  animateToTarget,
243
258
  resolveVariant,
244
259
  transition,
260
+ stopAnimation,
245
261
  };
246
- 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));
247
263
  const resolvedInitialValues = typeof initial === "string" ? resolveVariant(initial) : initial;
248
264
  const element = useRender(Component, Object.assign(Object.assign(Object.assign({}, restProps), gestureHandlers), { children }), ref, instanceRef, resolvedInitialValues);
249
265
  const getNextChildIndex = useCallback(() => {
@@ -7,6 +7,7 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
7
7
  * Create a callback ref that captures the Three.js instance
8
8
  */
9
9
  const callbackRef = useCallback((instance) => {
10
+ var _a;
10
11
  if (!instance)
11
12
  return;
12
13
  // Apply initial values immediately to prevent FOUC - but only once
@@ -45,6 +46,12 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
45
46
  colorProp.set(initialValues[key]);
46
47
  }
47
48
  }
49
+ else if (instance.uniforms &&
50
+ instance.uniforms[key] &&
51
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
52
+ // Set ShaderMaterial uniform initial value
53
+ instance.uniforms[key].value = initialValues[key];
54
+ }
48
55
  else if (key in instance && typeof instance[key] === "number") {
49
56
  instance[key] = initialValues[key];
50
57
  }
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.4",
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",