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 +195 -41
- package/dist/es/components/AnimatePresence/PresenceContext.mjs +22 -0
- package/dist/es/components/AnimatePresence/index.mjs +119 -0
- package/dist/es/index.mjs +2 -0
- package/dist/es/render/motion.mjs +45 -21
- package/dist/es/render/use-render.mjs +18 -21
- package/dist/index.d.ts +41 -2
- package/package.json +2 -1
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
|
-
//
|
|
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
|
|
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
|
|
437
|
-
animateColor(instance
|
|
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 === "
|
|
446
|
-
instance
|
|
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 {
|
|
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
|
|
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
|
|
170
|
-
animateColor(instance
|
|
179
|
+
else if (colorKeys.has(key) && instance[key]) {
|
|
180
|
+
animateColor(instance[key], value, opts, key);
|
|
171
181
|
}
|
|
172
|
-
else if (key
|
|
173
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
+
"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
|
},
|