r3f-motion 1.0.3 → 1.0.4

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,6 +8,24 @@ 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
  /**
@@ -19,7 +37,7 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
19
37
  // Apply initial values immediately to prevent FOUC - but only once
20
38
  if (initialValues && !initialValuesAppliedRef.current) {
21
39
  initialValuesAppliedRef.current = true;
22
- // Property mapping configuration
40
+ // Transform property mapping
23
41
  const propertyMap = {
24
42
  x: (val) => instance.position && (instance.position.x = val),
25
43
  y: (val) => instance.position && (instance.position.y = val),
@@ -32,32 +50,29 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
32
50
  scaleX: (val) => instance.scale && (instance.scale.x = val),
33
51
  scaleY: (val) => instance.scale && (instance.scale.y = val),
34
52
  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
53
  };
54
+ // Color-type properties that need .set()
55
+ const colorKeys = new Set([
56
+ "color",
57
+ "emissive",
58
+ "specular",
59
+ "sheenColor",
60
+ "attenuationColor",
61
+ ]);
56
62
  for (const key in initialValues) {
57
63
  const setter = propertyMap[key];
58
64
  if (setter) {
59
65
  setter(initialValues[key]);
60
66
  }
67
+ else if (colorKeys.has(key)) {
68
+ const colorProp = instance[key];
69
+ if (colorProp && colorProp.set) {
70
+ colorProp.set(initialValues[key]);
71
+ }
72
+ }
73
+ else if (key in instance && typeof instance[key] === "number") {
74
+ instance[key] = initialValues[key];
75
+ }
61
76
  }
62
77
  }
63
78
  // Store instance in the ref so animations can access it
@@ -286,7 +301,8 @@ function custom(Component) {
286
301
  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
302
  // eslint-disable-next-line react-hooks/exhaustive-deps
288
303
  []);
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"]);
304
+ const presenceContext = react.useContext(PresenceContext);
305
+ 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
306
  const initial = initialProp !== undefined
291
307
  ? initialProp
292
308
  : inherit && (parentContext === null || parentContext === void 0 ? void 0 : parentContext.initial);
@@ -422,6 +438,14 @@ function custom(Component) {
422
438
  const tempColor = new ColorConstructor(value);
423
439
  ["r", "g", "b"].forEach((channel) => createAnimation(target, { [channel]: tempColor[channel] }, opts, key));
424
440
  };
441
+ // Color-type properties that need RGB channel animation
442
+ const colorKeys = new Set([
443
+ "color",
444
+ "emissive",
445
+ "specular",
446
+ "sheenColor",
447
+ "attenuationColor",
448
+ ]);
425
449
  Object.entries(targetValues).forEach(([key, value]) => {
426
450
  const opts = getPropertyOpts(key);
427
451
  const mapping = transformMap[key];
@@ -433,26 +457,11 @@ function custom(Component) {
433
457
  createAnimation(mapping.target, { [mapping.prop]: value }, opts, key);
434
458
  }
435
459
  }
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);
441
- }
442
- else if (key === "opacity" && instance.opacity !== undefined) {
443
- createAnimation(instance, { opacity: value }, opts, key);
460
+ else if (colorKeys.has(key) && instance[key]) {
461
+ animateColor(instance[key], value, opts, key);
444
462
  }
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);
463
+ else if (key in instance && typeof instance[key] === "number") {
464
+ createAnimation(instance, { [key]: value }, opts, key);
456
465
  }
457
466
  });
458
467
  animationRef.current = {
@@ -479,6 +488,35 @@ function custom(Component) {
479
488
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
480
489
  // eslint-disable-next-line react-hooks/exhaustive-deps
481
490
  }, [animate]);
491
+ // Handle exit animation when presence context signals removal
492
+ react.useEffect(() => {
493
+ var _a;
494
+ if (!presenceContext || presenceContext.isPresent)
495
+ return;
496
+ if (!instanceRef.current || !exitProp) {
497
+ // No exit animation defined, safe to remove immediately
498
+ presenceContext.safeToRemove();
499
+ return;
500
+ }
501
+ const resolved = typeof exitProp === "string" ? resolveVariant(exitProp) : exitProp;
502
+ if (!resolved) {
503
+ presenceContext.safeToRemove();
504
+ return;
505
+ }
506
+ const resolvedObj = resolved;
507
+ const { transition: exitTransition } = resolvedObj, exitValues = tslib.__rest(resolvedObj, ["transition"]);
508
+ const effectiveExitTransition = exitTransition || transition;
509
+ // Store safeToRemove in a ref-like closure so onAnimationComplete can call it
510
+ const originalComplete = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAnimationComplete;
511
+ const safeToRemove = presenceContext.safeToRemove;
512
+ callbacksRef.current = Object.assign(Object.assign({}, callbacksRef.current), { onAnimationComplete: (variant) => {
513
+ originalComplete === null || originalComplete === void 0 ? void 0 : originalComplete(variant);
514
+ safeToRemove();
515
+ } });
516
+ animateToTarget(exitValues, effectiveExitTransition, true);
517
+ return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
518
+ // eslint-disable-next-line react-hooks/exhaustive-deps
519
+ }, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
482
520
  const gestureProps = {
483
521
  captureInstanceState,
484
522
  buildTargetFromState,
@@ -542,5 +580,121 @@ const MotionCamera = (props) => {
542
580
  return jsxRuntime.jsx(motion.primitive, Object.assign({ object: camera }, restProps));
543
581
  };
544
582
 
583
+ function getChildKey(child) {
584
+ var _a;
585
+ return (_a = child.key) !== null && _a !== void 0 ? _a : "";
586
+ }
587
+ function getValidChildren(children) {
588
+ const result = [];
589
+ const childArray = Array.isArray(children) ? children : [children];
590
+ for (const child of childArray) {
591
+ if (react.isValidElement(child)) {
592
+ result.push(child);
593
+ }
594
+ }
595
+ return result;
596
+ }
597
+ function AnimatePresence({ children, mode = "sync", onExitComplete, custom, }) {
598
+ const validChildren = getValidChildren(children);
599
+ const [entries, setEntries] = react.useState(() => validChildren.map((child) => ({
600
+ key: getChildKey(child),
601
+ element: child,
602
+ isPresent: true,
603
+ })));
604
+ const exitingCount = react.useRef(0);
605
+ const onExitCompleteRef = react.useRef(onExitComplete);
606
+ onExitCompleteRef.current = onExitComplete;
607
+ // Track whether we're waiting for exits to complete (for mode="wait")
608
+ const [isWaiting, setIsWaiting] = react.useState(false);
609
+ const pendingChildrenRef = react.useRef(null);
610
+ react.useEffect(() => {
611
+ const currentKeys = new Set(validChildren.map(getChildKey));
612
+ const prevKeys = new Set(entries.map((e) => e.key));
613
+ // Find new children to add
614
+ const entering = validChildren.filter((c) => !prevKeys.has(getChildKey(c)));
615
+ // Find children to remove
616
+ const exitingKeys = new Set(entries
617
+ .filter((e) => e.isPresent && !currentKeys.has(e.key))
618
+ .map((e) => e.key));
619
+ if (exitingKeys.size === 0 && entering.length === 0) {
620
+ // Just update existing children elements
621
+ setEntries((prev) => prev.map((entry) => {
622
+ const updated = validChildren.find((c) => getChildKey(c) === entry.key);
623
+ return updated ? Object.assign(Object.assign({}, entry), { element: updated }) : entry;
624
+ }));
625
+ return;
626
+ }
627
+ if (mode === "wait" && exitingKeys.size > 0 && entering.length > 0) {
628
+ // Store pending children, mark exits, wait for them to complete
629
+ pendingChildrenRef.current = validChildren;
630
+ setIsWaiting(true);
631
+ exitingCount.current = exitingKeys.size;
632
+ setEntries((prev) => prev.map((entry) => exitingKeys.has(entry.key)
633
+ ? Object.assign(Object.assign({}, entry), { isPresent: false }) : entry));
634
+ return;
635
+ }
636
+ exitingCount.current = exitingKeys.size;
637
+ setEntries((prev) => {
638
+ // Update existing, mark exiting
639
+ const updated = prev.map((entry) => {
640
+ if (exitingKeys.has(entry.key)) {
641
+ return Object.assign(Object.assign({}, entry), { isPresent: false });
642
+ }
643
+ const updatedChild = validChildren.find((c) => getChildKey(c) === entry.key);
644
+ return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
645
+ });
646
+ // Add entering children
647
+ const newEntries = entering.map((child) => ({
648
+ key: getChildKey(child),
649
+ element: child,
650
+ isPresent: true,
651
+ }));
652
+ return [...updated, ...newEntries];
653
+ });
654
+ // eslint-disable-next-line react-hooks/exhaustive-deps
655
+ }, [children]);
656
+ const handleSafeToRemove = (key) => {
657
+ var _a;
658
+ setEntries((prev) => prev.filter((entry) => entry.key !== key));
659
+ exitingCount.current--;
660
+ if (exitingCount.current <= 0) {
661
+ (_a = onExitCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onExitCompleteRef);
662
+ // If in wait mode and we have pending children, add them now
663
+ if (isWaiting && pendingChildrenRef.current) {
664
+ const pending = pendingChildrenRef.current;
665
+ pendingChildrenRef.current = null;
666
+ setIsWaiting(false);
667
+ setEntries((prev) => {
668
+ const remaining = prev; // exiting ones already filtered out above
669
+ const currentKeys = new Set(remaining.map((e) => e.key));
670
+ const newEntries = pending
671
+ .filter((c) => !currentKeys.has(getChildKey(c)))
672
+ .map((child) => ({
673
+ key: getChildKey(child),
674
+ element: child,
675
+ isPresent: true,
676
+ }));
677
+ // Update existing entries with new elements
678
+ const updated = remaining.map((entry) => {
679
+ const updatedChild = pending.find((c) => getChildKey(c) === entry.key);
680
+ return updatedChild ? Object.assign(Object.assign({}, entry), { element: updatedChild }) : entry;
681
+ });
682
+ return [...updated, ...newEntries];
683
+ });
684
+ }
685
+ }
686
+ };
687
+ return entries.map((entry) => {
688
+ const contextValue = {
689
+ isPresent: entry.isPresent,
690
+ safeToRemove: () => handleSafeToRemove(entry.key),
691
+ custom,
692
+ };
693
+ return react.createElement(PresenceContext.Provider, { key: entry.key, value: contextValue }, react.cloneElement(entry.element));
694
+ });
695
+ }
696
+
697
+ exports.AnimatePresence = AnimatePresence;
545
698
  exports.MotionCamera = MotionCamera;
546
699
  exports.motion = motion;
700
+ 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,6 +157,14 @@ 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]) => {
159
169
  const opts = getPropertyOpts(key);
160
170
  const mapping = transformMap[key];
@@ -166,26 +176,11 @@ function custom(Component) {
166
176
  createAnimation(mapping.target, { [mapping.prop]: value }, opts, key);
167
177
  }
168
178
  }
169
- else if (key === "color" && instance.color) {
170
- animateColor(instance.color, value, opts, key);
179
+ else if (colorKeys.has(key) && instance[key]) {
180
+ animateColor(instance[key], value, opts, key);
171
181
  }
172
- else if (key === "emissive" && instance.emissive) {
173
- animateColor(instance.emissive, value, opts, key);
174
- }
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);
182
+ else if (key in instance && typeof instance[key] === "number") {
183
+ createAnimation(instance, { [key]: value }, opts, key);
189
184
  }
190
185
  });
191
186
  animationRef.current = {
@@ -212,6 +207,35 @@ function custom(Component) {
212
207
  return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
213
208
  // eslint-disable-next-line react-hooks/exhaustive-deps
214
209
  }, [animate$1]);
210
+ // Handle exit animation when presence context signals removal
211
+ useEffect(() => {
212
+ var _a;
213
+ if (!presenceContext || presenceContext.isPresent)
214
+ return;
215
+ if (!instanceRef.current || !exitProp) {
216
+ // No exit animation defined, safe to remove immediately
217
+ presenceContext.safeToRemove();
218
+ return;
219
+ }
220
+ const resolved = typeof exitProp === "string" ? resolveVariant(exitProp) : exitProp;
221
+ if (!resolved) {
222
+ presenceContext.safeToRemove();
223
+ return;
224
+ }
225
+ const resolvedObj = resolved;
226
+ const { transition: exitTransition } = resolvedObj, exitValues = __rest(resolvedObj, ["transition"]);
227
+ const effectiveExitTransition = exitTransition || transition;
228
+ // Store safeToRemove in a ref-like closure so onAnimationComplete can call it
229
+ const originalComplete = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAnimationComplete;
230
+ const safeToRemove = presenceContext.safeToRemove;
231
+ callbacksRef.current = Object.assign(Object.assign({}, callbacksRef.current), { onAnimationComplete: (variant) => {
232
+ originalComplete === null || originalComplete === void 0 ? void 0 : originalComplete(variant);
233
+ safeToRemove();
234
+ } });
235
+ animateToTarget(exitValues, effectiveExitTransition, true);
236
+ return () => { var _a; return (_a = animationRef.current) === null || _a === void 0 ? void 0 : _a.stop(); };
237
+ // eslint-disable-next-line react-hooks/exhaustive-deps
238
+ }, [presenceContext === null || presenceContext === void 0 ? void 0 : presenceContext.isPresent]);
215
239
  const gestureProps = {
216
240
  captureInstanceState,
217
241
  buildTargetFromState,
@@ -12,7 +12,7 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
12
12
  // Apply initial values immediately to prevent FOUC - but only once
13
13
  if (initialValues && !initialValuesAppliedRef.current) {
14
14
  initialValuesAppliedRef.current = true;
15
- // Property mapping configuration
15
+ // Transform property mapping
16
16
  const propertyMap = {
17
17
  x: (val) => instance.position && (instance.position.x = val),
18
18
  y: (val) => instance.position && (instance.position.y = val),
@@ -25,32 +25,29 @@ const useRender = (Component, props, forwardedRef, instanceRef, initialValues) =
25
25
  scaleX: (val) => instance.scale && (instance.scale.x = val),
26
26
  scaleY: (val) => instance.scale && (instance.scale.y = val),
27
27
  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
28
  };
29
+ // Color-type properties that need .set()
30
+ const colorKeys = new Set([
31
+ "color",
32
+ "emissive",
33
+ "specular",
34
+ "sheenColor",
35
+ "attenuationColor",
36
+ ]);
49
37
  for (const key in initialValues) {
50
38
  const setter = propertyMap[key];
51
39
  if (setter) {
52
40
  setter(initialValues[key]);
53
41
  }
42
+ else if (colorKeys.has(key)) {
43
+ const colorProp = instance[key];
44
+ if (colorProp && colorProp.set) {
45
+ colorProp.set(initialValues[key]);
46
+ }
47
+ }
48
+ else if (key in instance && typeof instance[key] === "number") {
49
+ instance[key] = initialValues[key];
50
+ }
54
51
  }
55
52
  }
56
53
  // 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.4",
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
  },