r3f-motion 1.0.3 → 1.0.5

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
@@ -8,18 +8,37 @@ var jsxRuntime = require('react/jsx-runtime');
8
8
  var fiber = require('@react-three/fiber');
9
9
  var three = require('three');
10
10
 
11
+ const PresenceContext = react.createContext(null);
12
+ /**
13
+ * Returns presence information for the current component.
14
+ *
15
+ * - `isPresent`: Whether the component is currently present in the tree.
16
+ * When `false`, the component is exiting and should play its exit animation.
17
+ * - `safeToRemove`: Call this function when the exit animation is complete
18
+ * to signal that the component can be unmounted.
19
+ *
20
+ * Returns `null` if the component is not wrapped in `AnimatePresence`.
21
+ */
22
+ function usePresence() {
23
+ const context = react.useContext(PresenceContext);
24
+ if (!context)
25
+ return null;
26
+ return [context.isPresent, context.safeToRemove];
27
+ }
28
+
11
29
  const useRender = (Component, props, forwardedRef, instanceRef, initialValues) => {
12
30
  const initialValuesAppliedRef = react.useRef(false);
13
31
  /**
14
32
  * Create a callback ref that captures the Three.js instance
15
33
  */
16
34
  const callbackRef = react.useCallback((instance) => {
35
+ var _a;
17
36
  if (!instance)
18
37
  return;
19
38
  // Apply initial values immediately to prevent FOUC - but only once
20
39
  if (initialValues && !initialValuesAppliedRef.current) {
21
40
  initialValuesAppliedRef.current = true;
22
- // Property mapping configuration
41
+ // Transform property mapping
23
42
  const propertyMap = {
24
43
  x: (val) => instance.position && (instance.position.x = val),
25
44
  y: (val) => instance.position && (instance.position.y = val),
@@ -32,32 +51,35 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
32
51
  scaleX: (val) => instance.scale && (instance.scale.x = val),
33
52
  scaleY: (val) => instance.scale && (instance.scale.y = val),
34
53
  scaleZ: (val) => instance.scale && (instance.scale.z = val),
35
- color: (val) => {
36
- const color = instance.color;
37
- if (color && color.set) {
38
- color.set(val);
39
- }
40
- },
41
- opacity: (val) => instance.opacity !== undefined &&
42
- (instance.opacity = val),
43
- emissive: (val) => {
44
- const emissive = instance.emissive;
45
- if (emissive && emissive.set) {
46
- emissive.set(val);
47
- }
48
- },
49
- emissiveIntensity: (val) => instance.emissiveIntensity !== undefined &&
50
- (instance.emissiveIntensity = val),
51
- roughness: (val) => instance.roughness !== undefined &&
52
- (instance.roughness = val),
53
- metalness: (val) => instance.metalness !== undefined &&
54
- (instance.metalness = val),
55
54
  };
55
+ // Color-type properties that need .set()
56
+ const colorKeys = new Set([
57
+ "color",
58
+ "emissive",
59
+ "specular",
60
+ "sheenColor",
61
+ "attenuationColor",
62
+ ]);
56
63
  for (const key in initialValues) {
57
64
  const setter = propertyMap[key];
58
65
  if (setter) {
59
66
  setter(initialValues[key]);
60
67
  }
68
+ else if (colorKeys.has(key)) {
69
+ const colorProp = instance[key];
70
+ if (colorProp && colorProp.set) {
71
+ colorProp.set(initialValues[key]);
72
+ }
73
+ }
74
+ else if (instance.uniforms &&
75
+ instance.uniforms[key] &&
76
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
77
+ // Set ShaderMaterial uniform initial value
78
+ instance.uniforms[key].value = initialValues[key];
79
+ }
80
+ else if (key in instance && typeof instance[key] === "number") {
81
+ instance[key] = initialValues[key];
82
+ }
61
83
  }
62
84
  }
63
85
  // Store instance in the ref so animations can access it
@@ -286,7 +308,8 @@ function custom(Component) {
286
308
  const childIndex = react.useMemo(() => { var _a, _b; return (_b = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.getNextChildIndex) === null || _a === void 0 ? void 0 : _a.call(parentContext)) !== null && _b !== void 0 ? _b : 0; },
287
309
  // eslint-disable-next-line react-hooks/exhaustive-deps
288
310
  []);
289
- const _a = props, { initial: initialProp, animate: animateProp, transition, variants, custom, inherit = true, children, onAnimationUpdate, onAnimationStart, onAnimationComplete } = _a, restProps = tslib.__rest(_a, ["initial", "animate", "transition", "variants", "custom", "inherit", "children", "onAnimationUpdate", "onAnimationStart", "onAnimationComplete"]);
311
+ const presenceContext = react.useContext(PresenceContext);
312
+ const _a = props, { initial: initialProp, animate: animateProp, exit: exitProp, transition, variants, custom, inherit = true, children, onAnimationUpdate, onAnimationStart, onAnimationComplete } = _a, restProps = tslib.__rest(_a, ["initial", "animate", "exit", "transition", "variants", "custom", "inherit", "children", "onAnimationUpdate", "onAnimationStart", "onAnimationComplete"]);
290
313
  const initial = initialProp !== undefined
291
314
  ? initialProp
292
315
  : inherit && (parentContext === null || parentContext === void 0 ? void 0 : parentContext.initial);
@@ -422,7 +445,16 @@ function custom(Component) {
422
445
  const tempColor = new ColorConstructor(value);
423
446
  ["r", "g", "b"].forEach((channel) => createAnimation(target, { [channel]: tempColor[channel] }, opts, key));
424
447
  };
448
+ // Color-type properties that need RGB channel animation
449
+ const colorKeys = new Set([
450
+ "color",
451
+ "emissive",
452
+ "specular",
453
+ "sheenColor",
454
+ "attenuationColor",
455
+ ]);
425
456
  Object.entries(targetValues).forEach(([key, value]) => {
457
+ var _a;
426
458
  const opts = getPropertyOpts(key);
427
459
  const mapping = transformMap[key];
428
460
  if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
@@ -433,26 +465,19 @@ function custom(Component) {
433
465
  createAnimation(mapping.target, { [mapping.prop]: value }, opts, key);
434
466
  }
435
467
  }
436
- else if (key === "color" && instance.color) {
437
- animateColor(instance.color, value, opts, key);
438
- }
439
- else if (key === "emissive" && instance.emissive) {
440
- animateColor(instance.emissive, value, opts, key);
468
+ else if (colorKeys.has(key) && instance[key]) {
469
+ animateColor(instance[key], value, opts, key);
441
470
  }
442
- else if (key === "opacity" && instance.opacity !== undefined) {
443
- createAnimation(instance, { opacity: value }, opts, key);
471
+ else if (instance.uniforms &&
472
+ instance.uniforms[key] &&
473
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
474
+ // Animate ShaderMaterial uniforms (uniforms[key].value)
475
+ // Note: consumers must useMemo their uniforms prop to prevent
476
+ // R3F from overwriting animated values on re-render
477
+ createAnimation(instance.uniforms[key], { value: value }, opts, key);
444
478
  }
445
- else if (key === "emissiveIntensity" &&
446
- instance.emissiveIntensity !== undefined) {
447
- createAnimation(instance, { emissiveIntensity: value }, opts, key);
448
- }
449
- else if (key === "roughness" &&
450
- instance.roughness !== undefined) {
451
- createAnimation(instance, { roughness: value }, opts, key);
452
- }
453
- else if (key === "metalness" &&
454
- instance.metalness !== undefined) {
455
- createAnimation(instance, { metalness: value }, opts, key);
479
+ else if (key in instance && typeof instance[key] === "number") {
480
+ createAnimation(instance, { [key]: value }, opts, key);
456
481
  }
457
482
  });
458
483
  animationRef.current = {
@@ -479,6 +504,35 @@ function custom(Component) {
479
504
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
480
505
  // eslint-disable-next-line react-hooks/exhaustive-deps
481
506
  }, [animate]);
507
+ // Handle exit animation when presence context signals removal
508
+ react.useEffect(() => {
509
+ var _a;
510
+ if (!presenceContext || presenceContext.isPresent)
511
+ return;
512
+ if (!instanceRef.current || !exitProp) {
513
+ // No exit animation defined, safe to remove immediately
514
+ presenceContext.safeToRemove();
515
+ return;
516
+ }
517
+ const resolved = typeof exitProp === "string" ? resolveVariant(exitProp) : exitProp;
518
+ if (!resolved) {
519
+ presenceContext.safeToRemove();
520
+ return;
521
+ }
522
+ const resolvedObj = resolved;
523
+ const { transition: exitTransition } = resolvedObj, exitValues = tslib.__rest(resolvedObj, ["transition"]);
524
+ const effectiveExitTransition = exitTransition || transition;
525
+ // Store safeToRemove in a ref-like closure so onAnimationComplete can call it
526
+ const originalComplete = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAnimationComplete;
527
+ const safeToRemove = presenceContext.safeToRemove;
528
+ callbacksRef.current = Object.assign(Object.assign({}, callbacksRef.current), { onAnimationComplete: (variant) => {
529
+ originalComplete === null || originalComplete === void 0 ? void 0 : originalComplete(variant);
530
+ safeToRemove();
531
+ } });
532
+ animateToTarget(exitValues, effectiveExitTransition, true);
533
+ return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
534
+ // eslint-disable-next-line react-hooks/exhaustive-deps
535
+ }, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
482
536
  const gestureProps = {
483
537
  captureInstanceState,
484
538
  buildTargetFromState,
@@ -542,5 +596,121 @@ const MotionCamera = (props) => {
542
596
  return jsxRuntime.jsx(motion.primitive, Object.assign({ object: camera }, restProps));
543
597
  };
544
598
 
599
+ function getChildKey(child) {
600
+ var _a;
601
+ return (_a = child.key) !== null && _a !== void 0 ? _a : "";
602
+ }
603
+ function getValidChildren(children) {
604
+ const result = [];
605
+ const childArray = Array.isArray(children) ? children : [children];
606
+ for (const child of childArray) {
607
+ if (react.isValidElement(child)) {
608
+ result.push(child);
609
+ }
610
+ }
611
+ return result;
612
+ }
613
+ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
614
+ const validChildren = getValidChildren(children);
615
+ const [entries, setEntries] = react.useState(() => validChildren.map((child) => ({
616
+ key: getChildKey(child),
617
+ element: child,
618
+ isPresent: true,
619
+ })));
620
+ const exitingCount = react.useRef(0);
621
+ const onExitCompleteRef = react.useRef(onExitComplete);
622
+ onExitCompleteRef.current = onExitComplete;
623
+ // Track whether we're waiting for exits to complete (for mode="wait")
624
+ const [isWaiting, setIsWaiting] = react.useState(false);
625
+ const pendingChildrenRef = react.useRef(null);
626
+ react.useEffect(() => {
627
+ const currentKeys = new Set(validChildren.map(getChildKey));
628
+ const prevKeys = new Set(entries.map((e) => e.key));
629
+ // Find new children to add
630
+ const entering = validChildren.filter((c) => !prevKeys.has(getChildKey(c)));
631
+ // Find children to remove
632
+ const exitingKeys = new Set(entries
633
+ .filter((e) => e.isPresent && !currentKeys.has(e.key))
634
+ .map((e) => e.key));
635
+ if (exitingKeys.size === 0 && entering.length === 0) {
636
+ // Just update existing children elements
637
+ setEntries((prev) => prev.map((entry) => {
638
+ const updated = validChildren.find((c) => getChildKey(c) === entry.key);
639
+ return updated ? Object.assign(Object.assign({}, entry), { element: updated }) : entry;
640
+ }));
641
+ return;
642
+ }
643
+ if (mode === "wait" && exitingKeys.size > 0 && entering.length > 0) {
644
+ // Store pending children, mark exits, wait for them to complete
645
+ pendingChildrenRef.current = validChildren;
646
+ setIsWaiting(true);
647
+ exitingCount.current = exitingKeys.size;
648
+ setEntries((prev) => prev.map((entry) => exitingKeys.has(entry.key)
649
+ ? Object.assign(Object.assign({}, entry), { isPresent: false }) : entry));
650
+ return;
651
+ }
652
+ exitingCount.current = exitingKeys.size;
653
+ setEntries((prev) => {
654
+ // Update existing, mark exiting
655
+ const updated = prev.map((entry) => {
656
+ if (exitingKeys.has(entry.key)) {
657
+ return Object.assign(Object.assign({}, entry), { isPresent: false });
658
+ }
659
+ const updatedChild = validChildren.find((c) => getChildKey(c) === entry.key);
660
+ return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
661
+ });
662
+ // Add entering children
663
+ const newEntries = entering.map((child) => ({
664
+ key: getChildKey(child),
665
+ element: child,
666
+ isPresent: true,
667
+ }));
668
+ return [...updated, ...newEntries];
669
+ });
670
+ // eslint-disable-next-line react-hooks/exhaustive-deps
671
+ }, [children]);
672
+ const handleSafeToRemove = (key) => {
673
+ var _a;
674
+ setEntries((prev) => prev.filter((entry) => entry.key !== key));
675
+ exitingCount.current--;
676
+ if (exitingCount.current <= 0) {
677
+ (_a = onExitCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onExitCompleteRef);
678
+ // If in wait mode and we have pending children, add them now
679
+ if (isWaiting && pendingChildrenRef.current) {
680
+ const pending = pendingChildrenRef.current;
681
+ pendingChildrenRef.current = null;
682
+ setIsWaiting(false);
683
+ setEntries((prev) => {
684
+ const remaining = prev; // exiting ones already filtered out above
685
+ const currentKeys = new Set(remaining.map((e) => e.key));
686
+ const newEntries = pending
687
+ .filter((c) => !currentKeys.has(getChildKey(c)))
688
+ .map((child) => ({
689
+ key: getChildKey(child),
690
+ element: child,
691
+ isPresent: true,
692
+ }));
693
+ // Update existing entries with new elements
694
+ const updated = remaining.map((entry) => {
695
+ const updatedChild = pending.find((c) => getChildKey(c) === entry.key);
696
+ return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
697
+ });
698
+ return [...updated, ...newEntries];
699
+ });
700
+ }
701
+ }
702
+ };
703
+ return entries.map((entry) => {
704
+ const contextValue = {
705
+ isPresent: entry.isPresent,
706
+ safeToRemove: () => handleSafeToRemove(entry.key),
707
+ custom,
708
+ };
709
+ return react.createElement(PresenceContext.Provider, { key: entry.key, value: contextValue }, react.cloneElement(entry.element));
710
+ });
711
+ }
712
+
713
+ exports.AnimatePresence = AnimatePresence;
545
714
  exports.MotionCamera = MotionCamera;
546
715
  exports.motion = motion;
716
+ exports.usePresence = usePresence;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { createContext, useContext } from 'react';
3
+
4
+ const PresenceContext = createContext(null);
5
+ /**
6
+ * Returns presence information for the current component.
7
+ *
8
+ * - `isPresent`: Whether the component is currently present in the tree.
9
+ * When `false`, the component is exiting and should play its exit animation.
10
+ * - `safeToRemove`: Call this function when the exit animation is complete
11
+ * to signal that the component can be unmounted.
12
+ *
13
+ * Returns `null` if the component is not wrapped in `AnimatePresence`.
14
+ */
15
+ function usePresence() {
16
+ const context = useContext(PresenceContext);
17
+ if (!context)
18
+ return null;
19
+ return [context.isPresent, context.safeToRemove];
20
+ }
21
+
22
+ export { PresenceContext, usePresence };
@@ -0,0 +1,119 @@
1
+ "use client";
2
+ import { useState, useRef, useEffect, createElement, cloneElement, isValidElement } from 'react';
3
+ import { PresenceContext } from './PresenceContext.mjs';
4
+
5
+ function getChildKey(child) {
6
+ var _a;
7
+ return (_a = child.key) !== null && _a !== void 0 ? _a : "";
8
+ }
9
+ function getValidChildren(children) {
10
+ const result = [];
11
+ const childArray = Array.isArray(children) ? children : [children];
12
+ for (const child of childArray) {
13
+ if (isValidElement(child)) {
14
+ result.push(child);
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
20
+ const validChildren = getValidChildren(children);
21
+ const [entries, setEntries] = useState(() => validChildren.map((child) => ({
22
+ key: getChildKey(child),
23
+ element: child,
24
+ isPresent: true,
25
+ })));
26
+ const exitingCount = useRef(0);
27
+ const onExitCompleteRef = useRef(onExitComplete);
28
+ onExitCompleteRef.current = onExitComplete;
29
+ // Track whether we're waiting for exits to complete (for mode="wait")
30
+ const [isWaiting, setIsWaiting] = useState(false);
31
+ const pendingChildrenRef = useRef(null);
32
+ useEffect(() => {
33
+ const currentKeys = new Set(validChildren.map(getChildKey));
34
+ const prevKeys = new Set(entries.map((e) => e.key));
35
+ // Find new children to add
36
+ const entering = validChildren.filter((c) => !prevKeys.has(getChildKey(c)));
37
+ // Find children to remove
38
+ const exitingKeys = new Set(entries
39
+ .filter((e) => e.isPresent && !currentKeys.has(e.key))
40
+ .map((e) => e.key));
41
+ if (exitingKeys.size === 0 && entering.length === 0) {
42
+ // Just update existing children elements
43
+ setEntries((prev) => prev.map((entry) => {
44
+ const updated = validChildren.find((c) => getChildKey(c) === entry.key);
45
+ return updated ? Object.assign(Object.assign({}, entry), { element: updated }) : entry;
46
+ }));
47
+ return;
48
+ }
49
+ if (mode === "wait" && exitingKeys.size > 0 && entering.length > 0) {
50
+ // Store pending children, mark exits, wait for them to complete
51
+ pendingChildrenRef.current = validChildren;
52
+ setIsWaiting(true);
53
+ exitingCount.current = exitingKeys.size;
54
+ setEntries((prev) => prev.map((entry) => exitingKeys.has(entry.key)
55
+ ? Object.assign(Object.assign({}, entry), { isPresent: false }) : entry));
56
+ return;
57
+ }
58
+ exitingCount.current = exitingKeys.size;
59
+ setEntries((prev) => {
60
+ // Update existing, mark exiting
61
+ const updated = prev.map((entry) => {
62
+ if (exitingKeys.has(entry.key)) {
63
+ return Object.assign(Object.assign({}, entry), { isPresent: false });
64
+ }
65
+ const updatedChild = validChildren.find((c) => getChildKey(c) === entry.key);
66
+ return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
67
+ });
68
+ // Add entering children
69
+ const newEntries = entering.map((child) => ({
70
+ key: getChildKey(child),
71
+ element: child,
72
+ isPresent: true,
73
+ }));
74
+ return [...updated, ...newEntries];
75
+ });
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps
77
+ }, [children]);
78
+ const handleSafeToRemove = (key) => {
79
+ var _a;
80
+ setEntries((prev) => prev.filter((entry) => entry.key !== key));
81
+ exitingCount.current--;
82
+ if (exitingCount.current <= 0) {
83
+ (_a = onExitCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onExitCompleteRef);
84
+ // If in wait mode and we have pending children, add them now
85
+ if (isWaiting && pendingChildrenRef.current) {
86
+ const pending = pendingChildrenRef.current;
87
+ pendingChildrenRef.current = null;
88
+ setIsWaiting(false);
89
+ setEntries((prev) => {
90
+ const remaining = prev; // exiting ones already filtered out above
91
+ const currentKeys = new Set(remaining.map((e) => e.key));
92
+ const newEntries = pending
93
+ .filter((c) => !currentKeys.has(getChildKey(c)))
94
+ .map((child) => ({
95
+ key: getChildKey(child),
96
+ element: child,
97
+ isPresent: true,
98
+ }));
99
+ // Update existing entries with new elements
100
+ const updated = remaining.map((entry) => {
101
+ const updatedChild = pending.find((c) => getChildKey(c) === entry.key);
102
+ return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
103
+ });
104
+ return [...updated, ...newEntries];
105
+ });
106
+ }
107
+ }
108
+ };
109
+ return entries.map((entry) => {
110
+ const contextValue = {
111
+ isPresent: entry.isPresent,
112
+ safeToRemove: () => handleSafeToRemove(entry.key),
113
+ custom,
114
+ };
115
+ return createElement(PresenceContext.Provider, { key: entry.key, value: contextValue }, cloneElement(entry.element));
116
+ });
117
+ }
118
+
119
+ export { AnimatePresence };
package/dist/es/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
1
  "use client";
2
2
  export { motion } from './render/motion.mjs';
3
3
  export { default as MotionCamera } from './components/MotionCamera.mjs';
4
+ export { AnimatePresence } from './components/AnimatePresence/index.mjs';
5
+ export { usePresence } from './components/AnimatePresence/PresenceContext.mjs';
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { __rest } from 'tslib';
3
- import { createContext, forwardRef, useRef, useContext, useMemo, useCallback, useEffect, createElement, memo } from 'react';
3
+ import { forwardRef, useRef, useContext, useMemo, useCallback, useEffect, createElement, memo, createContext } from 'react';
4
4
  import { animate } from 'motion';
5
+ import { PresenceContext } from '../components/AnimatePresence/PresenceContext.mjs';
5
6
  import { useRender } from './use-render.mjs';
6
7
  import { useHover } from './gestures/use-hover.mjs';
7
8
  import { useTap } from './gestures/use-tap.mjs';
@@ -19,7 +20,8 @@ function custom(Component) {
19
20
  const childIndex = useMemo(() => { var _a, _b; return (_b = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.getNextChildIndex) === null || _a === void 0 ? void 0 : _a.call(parentContext)) !== null && _b !== void 0 ? _b : 0; },
20
21
  // eslint-disable-next-line react-hooks/exhaustive-deps
21
22
  []);
22
- const _a = props, { initial: initialProp, animate: animateProp, transition, variants, custom, inherit = true, children, onAnimationUpdate, onAnimationStart, onAnimationComplete } = _a, restProps = __rest(_a, ["initial", "animate", "transition", "variants", "custom", "inherit", "children", "onAnimationUpdate", "onAnimationStart", "onAnimationComplete"]);
23
+ const presenceContext = useContext(PresenceContext);
24
+ const _a = props, { initial: initialProp, animate: animateProp, exit: exitProp, transition, variants, custom, inherit = true, children, onAnimationUpdate, onAnimationStart, onAnimationComplete } = _a, restProps = __rest(_a, ["initial", "animate", "exit", "transition", "variants", "custom", "inherit", "children", "onAnimationUpdate", "onAnimationStart", "onAnimationComplete"]);
23
25
  const initial = initialProp !== undefined
24
26
  ? initialProp
25
27
  : inherit && (parentContext === null || parentContext === void 0 ? void 0 : parentContext.initial);
@@ -155,7 +157,16 @@ function custom(Component) {
155
157
  const tempColor = new ColorConstructor(value);
156
158
  ["r", "g", "b"].forEach((channel) => createAnimation(target, { [channel]: tempColor[channel] }, opts, key));
157
159
  };
160
+ // Color-type properties that need RGB channel animation
161
+ const colorKeys = new Set([
162
+ "color",
163
+ "emissive",
164
+ "specular",
165
+ "sheenColor",
166
+ "attenuationColor",
167
+ ]);
158
168
  Object.entries(targetValues).forEach(([key, value]) => {
169
+ var _a;
159
170
  const opts = getPropertyOpts(key);
160
171
  const mapping = transformMap[key];
161
172
  if (mapping === null || mapping === void 0 ? void 0 : mapping.target) {
@@ -166,26 +177,19 @@ function custom(Component) {
166
177
  createAnimation(mapping.target, { [mapping.prop]: value }, opts, key);
167
178
  }
168
179
  }
169
- else if (key === "color" && instance.color) {
170
- animateColor(instance.color, value, opts, key);
180
+ else if (colorKeys.has(key) && instance[key]) {
181
+ animateColor(instance[key], value, opts, key);
171
182
  }
172
- else if (key === "emissive" && instance.emissive) {
173
- animateColor(instance.emissive, value, opts, key);
183
+ else if (instance.uniforms &&
184
+ instance.uniforms[key] &&
185
+ typeof ((_a = instance.uniforms[key]) === null || _a === void 0 ? void 0 : _a.value) === "number") {
186
+ // Animate ShaderMaterial uniforms (uniforms[key].value)
187
+ // Note: consumers must useMemo their uniforms prop to prevent
188
+ // R3F from overwriting animated values on re-render
189
+ createAnimation(instance.uniforms[key], { value: value }, opts, key);
174
190
  }
175
- else if (key === "opacity" && instance.opacity !== undefined) {
176
- createAnimation(instance, { opacity: value }, opts, key);
177
- }
178
- else if (key === "emissiveIntensity" &&
179
- instance.emissiveIntensity !== undefined) {
180
- createAnimation(instance, { emissiveIntensity: value }, opts, key);
181
- }
182
- else if (key === "roughness" &&
183
- instance.roughness !== undefined) {
184
- createAnimation(instance, { roughness: value }, opts, key);
185
- }
186
- else if (key === "metalness" &&
187
- instance.metalness !== undefined) {
188
- createAnimation(instance, { metalness: value }, opts, key);
191
+ else if (key in instance && typeof instance[key] === "number") {
192
+ createAnimation(instance, { [key]: value }, opts, key);
189
193
  }
190
194
  });
191
195
  animationRef.current = {
@@ -212,6 +216,35 @@ function custom(Component) {
212
216
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
213
217
  // eslint-disable-next-line react-hooks/exhaustive-deps
214
218
  }, [animate$1]);
219
+ // Handle exit animation when presence context signals removal
220
+ useEffect(() => {
221
+ var _a;
222
+ if (!presenceContext || presenceContext.isPresent)
223
+ return;
224
+ if (!instanceRef.current || !exitProp) {
225
+ // No exit animation defined, safe to remove immediately
226
+ presenceContext.safeToRemove();
227
+ return;
228
+ }
229
+ const resolved = typeof exitProp === "string" ? resolveVariant(exitProp) : exitProp;
230
+ if (!resolved) {
231
+ presenceContext.safeToRemove();
232
+ return;
233
+ }
234
+ const resolvedObj = resolved;
235
+ const { transition: exitTransition } = resolvedObj, exitValues = __rest(resolvedObj, ["transition"]);
236
+ const effectiveExitTransition = exitTransition || transition;
237
+ // Store safeToRemove in a ref-like closure so onAnimationComplete can call it
238
+ const originalComplete = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAnimationComplete;
239
+ const safeToRemove = presenceContext.safeToRemove;
240
+ callbacksRef.current = Object.assign(Object.assign({}, callbacksRef.current), { onAnimationComplete: (variant) => {
241
+ originalComplete === null || originalComplete === void 0 ? void 0 : originalComplete(variant);
242
+ safeToRemove();
243
+ } });
244
+ animateToTarget(exitValues, effectiveExitTransition, true);
245
+ return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
246
+ // eslint-disable-next-line react-hooks/exhaustive-deps
247
+ }, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
215
248
  const gestureProps = {
216
249
  captureInstanceState,
217
250
  buildTargetFromState,
@@ -7,12 +7,13 @@ 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
13
14
  if (initialValues && !initialValuesAppliedRef.current) {
14
15
  initialValuesAppliedRef.current = true;
15
- // Property mapping configuration
16
+ // Transform property mapping
16
17
  const propertyMap = {
17
18
  x: (val) => instance.position && (instance.position.x = val),
18
19
  y: (val) => instance.position && (instance.position.y = val),
@@ -25,32 +26,35 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
25
26
  scaleX: (val) => instance.scale && (instance.scale.x = val),
26
27
  scaleY: (val) => instance.scale && (instance.scale.y = val),
27
28
  scaleZ: (val) => instance.scale && (instance.scale.z = val),
28
- color: (val) => {
29
- const color = instance.color;
30
- if (color && color.set) {
31
- color.set(val);
32
- }
33
- },
34
- opacity: (val) => instance.opacity !== undefined &&
35
- (instance.opacity = val),
36
- emissive: (val) => {
37
- const emissive = instance.emissive;
38
- if (emissive && emissive.set) {
39
- emissive.set(val);
40
- }
41
- },
42
- emissiveIntensity: (val) => instance.emissiveIntensity !== undefined &&
43
- (instance.emissiveIntensity = val),
44
- roughness: (val) => instance.roughness !== undefined &&
45
- (instance.roughness = val),
46
- metalness: (val) => instance.metalness !== undefined &&
47
- (instance.metalness = val),
48
29
  };
30
+ // Color-type properties that need .set()
31
+ const colorKeys = new Set([
32
+ "color",
33
+ "emissive",
34
+ "specular",
35
+ "sheenColor",
36
+ "attenuationColor",
37
+ ]);
49
38
  for (const key in initialValues) {
50
39
  const setter = propertyMap[key];
51
40
  if (setter) {
52
41
  setter(initialValues[key]);
53
42
  }
43
+ else if (colorKeys.has(key)) {
44
+ const colorProp = instance[key];
45
+ if (colorProp && colorProp.set) {
46
+ colorProp.set(initialValues[key]);
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
+ }
55
+ else if (key in instance && typeof instance[key] === "number") {
56
+ instance[key] = initialValues[key];
57
+ }
54
58
  }
55
59
  }
56
60
  // Store instance in the ref so animations can access it
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Vector3, Euler, Color, ReactThreeFiber } from '@react-three/fiber';
2
2
  import * as THREE from 'three';
3
- import { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes } from 'react';
3
+ import * as react from 'react';
4
+ import { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes, ReactNode } from 'react';
4
5
  import { MotionValue, MotionProps, ResolvedValues } from 'motion/react';
5
6
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
7
 
@@ -51,5 +52,43 @@ interface MotionCameraProps extends ThreeMotionProps {
51
52
  }
52
53
  declare const MotionCamera: (props: MotionCameraProps) => react_jsx_runtime.JSX.Element;
53
54
 
54
- export { MotionCamera, motion };
55
+ interface PresenceContextValue {
56
+ isPresent: boolean;
57
+ safeToRemove: () => void;
58
+ custom?: unknown;
59
+ }
60
+ /**
61
+ * Returns presence information for the current component.
62
+ *
63
+ * - `isPresent`: Whether the component is currently present in the tree.
64
+ * When `false`, the component is exiting and should play its exit animation.
65
+ * - `safeToRemove`: Call this function when the exit animation is complete
66
+ * to signal that the component can be unmounted.
67
+ *
68
+ * Returns `null` if the component is not wrapped in `AnimatePresence`.
69
+ */
70
+ declare function usePresence(): [boolean, () => void] | null;
71
+
72
+ interface AnimatePresenceProps {
73
+ children?: ReactNode;
74
+ /**
75
+ * If `true`, `AnimatePresence` will only render one component
76
+ * at a time. The exiting component will finish its exit animation
77
+ * before the entering component is rendered.
78
+ *
79
+ * @default "sync"
80
+ */
81
+ mode?: "sync" | "wait";
82
+ /**
83
+ * Fires when all exiting children have finished animating out.
84
+ */
85
+ onExitComplete?: () => void;
86
+ /**
87
+ * Custom data to pass to exiting children via the presence context.
88
+ */
89
+ custom?: unknown;
90
+ }
91
+ declare function AnimatePresence({ children, mode, onExitComplete, custom, }: AnimatePresenceProps): react.FunctionComponentElement<react.ProviderProps<PresenceContextValue | null>>[];
92
+
93
+ export { AnimatePresence, MotionCamera, motion, usePresence };
55
94
  export type { AcceptMotionValues, ForwardRefComponent, ThreeElement, ThreeMotionComponents, ThreeMotionProps, ThreeRenderState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "r3f-motion",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -50,6 +50,7 @@
50
50
  "postpublish": "git push --tags"
51
51
  },
52
52
  "dependencies": {
53
+ "@storybook/addon-docs": "^10.2.10",
53
54
  "motion": "^12.29.0",
54
55
  "react-merge-refs": "^3.0.2"
55
56
  },