@aphrody/m3-motion 3.3.2 → 3.3.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/components/M3Collapse.d.ts.map +1 -1
- package/dist/components/M3ContainerTransform.d.ts +6 -6
- package/dist/components/M3ContainerTransform.d.ts.map +1 -1
- package/dist/components/M3Fade.d.ts.map +1 -1
- package/dist/components/M3FadeThrough.d.ts.map +1 -1
- package/dist/components/M3ListStagger.d.ts.map +1 -1
- package/dist/components/M3SharedAxis.d.ts.map +1 -1
- package/dist/components/M3Transition.d.ts.map +1 -1
- package/dist/easings.d.ts +1 -1
- package/dist/hooks/useM3Animate.d.ts.map +1 -1
- package/dist/hooks/useM3ReducedMotion.d.ts +6 -0
- package/dist/hooks/useM3ReducedMotion.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +398 -338
- package/dist/index.js.map +21 -1
- package/package.json +12 -13
- package/src/components/M3Collapse.tsx +82 -0
- package/src/components/M3ContainerTransform.tsx +187 -0
- package/src/components/M3Fade.tsx +68 -0
- package/src/components/M3FadeThrough.tsx +80 -0
- package/src/components/M3ListStagger.tsx +117 -0
- package/src/components/M3SharedAxis.tsx +115 -0
- package/src/components/M3Transition.tsx +207 -0
- package/src/easings.ts +62 -0
- package/src/hooks/useM3Animate.ts +93 -0
- package/src/hooks/useM3ReducedMotion.ts +43 -0
- package/src/index.ts +12 -0
- package/src/spring-interpolation.ts +390 -0
- package/src/springs.ts +54 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { motion } from "motion/react";
|
|
3
|
+
import type { HTMLMotionProps, Transition, MotionStyle } from "motion/react";
|
|
4
|
+
import { m3Springs } from "../springs.js";
|
|
5
|
+
import { m3Easings } from "../easings.js";
|
|
6
|
+
import { useM3ReducedMotion } from "../hooks/useM3ReducedMotion.js";
|
|
7
|
+
|
|
8
|
+
export interface M3ContainerTransformProps extends HTMLMotionProps<"div"> {
|
|
9
|
+
/**
|
|
10
|
+
* Shared identifier linking the starting container (e.g., card) and ending container (e.g., detail page).
|
|
11
|
+
*/
|
|
12
|
+
layoutId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Speed preset to use for the morphing animation.
|
|
15
|
+
* @default "default"
|
|
16
|
+
*/
|
|
17
|
+
speed?: "fast" | "default" | "slow";
|
|
18
|
+
/**
|
|
19
|
+
* If true, renders the expanded container state.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
isExpanded?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface M3ContainerTransformContextValue {
|
|
26
|
+
isExpanded: boolean;
|
|
27
|
+
speed: "fast" | "default" | "slow";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const M3ContainerTransformContext =
|
|
31
|
+
React.createContext<M3ContainerTransformContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Helper function to generate spec-compliant cross-fade timings.
|
|
35
|
+
*/
|
|
36
|
+
function getDefaultTransition(isActive: boolean, speed: "fast" | "default" | "slow"): Transition {
|
|
37
|
+
if (speed === "fast") {
|
|
38
|
+
return isActive
|
|
39
|
+
? { delay: 0.05, duration: 0.15, ease: m3Easings.emphasizedDecelerate }
|
|
40
|
+
: { duration: 0.08, ease: m3Easings.emphasizedAccelerate };
|
|
41
|
+
}
|
|
42
|
+
if (speed === "slow") {
|
|
43
|
+
return isActive
|
|
44
|
+
? { delay: 0.2, duration: 0.6, ease: m3Easings.emphasizedDecelerate }
|
|
45
|
+
: { duration: 0.25, ease: m3Easings.emphasizedAccelerate };
|
|
46
|
+
}
|
|
47
|
+
// Default speed
|
|
48
|
+
return isActive
|
|
49
|
+
? { delay: 0.1, duration: 0.35, ease: m3Easings.emphasizedDecelerate }
|
|
50
|
+
: { duration: 0.15, ease: m3Easings.emphasizedAccelerate };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* M3ContainerTransform component.
|
|
55
|
+
* Implements the Material 3 Container Transform motion pattern using shared element transitions.
|
|
56
|
+
* Use the same layoutId on both source and target containers to morph them smoothly.
|
|
57
|
+
*/
|
|
58
|
+
export const M3ContainerTransform = React.forwardRef<HTMLDivElement, M3ContainerTransformProps>(
|
|
59
|
+
(
|
|
60
|
+
{
|
|
61
|
+
layoutId,
|
|
62
|
+
speed = "default",
|
|
63
|
+
isExpanded = false,
|
|
64
|
+
children,
|
|
65
|
+
transition,
|
|
66
|
+
style,
|
|
67
|
+
...props
|
|
68
|
+
}: M3ContainerTransformProps,
|
|
69
|
+
ref,
|
|
70
|
+
) => {
|
|
71
|
+
const reduced = useM3ReducedMotion();
|
|
72
|
+
// Select the M3 spatial spring configuration based on speed
|
|
73
|
+
const springConfig = m3Springs[speed].spatial;
|
|
74
|
+
|
|
75
|
+
const contextValue = React.useMemo(() => ({ isExpanded, speed }), [isExpanded, speed]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<M3ContainerTransformContext.Provider value={contextValue}>
|
|
79
|
+
<motion.div
|
|
80
|
+
ref={ref}
|
|
81
|
+
layoutId={reduced ? undefined : layoutId}
|
|
82
|
+
layout={!reduced}
|
|
83
|
+
transition={
|
|
84
|
+
reduced ? { type: "tween", duration: 0, delay: 0 } : transition || springConfig
|
|
85
|
+
}
|
|
86
|
+
style={{
|
|
87
|
+
position: "relative",
|
|
88
|
+
willChange: "transform, border-radius",
|
|
89
|
+
overflow: "hidden",
|
|
90
|
+
...style,
|
|
91
|
+
}}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</motion.div>
|
|
96
|
+
</M3ContainerTransformContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
M3ContainerTransform.displayName = "M3ContainerTransform";
|
|
102
|
+
|
|
103
|
+
export interface M3ContainerTransformStartProps extends HTMLMotionProps<"div"> {
|
|
104
|
+
/**
|
|
105
|
+
* Overrides the active state. By default, active when isExpanded is false.
|
|
106
|
+
*/
|
|
107
|
+
active?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface M3ContainerTransformEndProps extends HTMLMotionProps<"div"> {
|
|
111
|
+
/**
|
|
112
|
+
* Overrides the active state. By default, active when isExpanded is true.
|
|
113
|
+
*/
|
|
114
|
+
active?: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One cross-fading layer of the container transform. `start` is shown while collapsed, `end` while
|
|
119
|
+
* expanded; the inactive layer is taken out of the flow and stops receiving pointer events, so the
|
|
120
|
+
* content never distorts during the morph.
|
|
121
|
+
*/
|
|
122
|
+
const ContainerTransformLayer = React.forwardRef<
|
|
123
|
+
HTMLDivElement,
|
|
124
|
+
M3ContainerTransformStartProps & { layer: "start" | "end" }
|
|
125
|
+
>(({ layer, active, children, style, transition, layout = "position", ...props }, ref) => {
|
|
126
|
+
const reduced = useM3ReducedMotion();
|
|
127
|
+
const context = React.useContext(M3ContainerTransformContext);
|
|
128
|
+
const isActive = active ?? (context ? context.isExpanded === (layer === "end") : true);
|
|
129
|
+
const speed = context?.speed ?? "default";
|
|
130
|
+
|
|
131
|
+
const baseStyle: MotionStyle = {
|
|
132
|
+
width: "100%",
|
|
133
|
+
height: "100%",
|
|
134
|
+
boxSizing: "border-box",
|
|
135
|
+
pointerEvents: isActive ? "auto" : "none",
|
|
136
|
+
...(!isActive && {
|
|
137
|
+
position: "absolute",
|
|
138
|
+
top: 0,
|
|
139
|
+
left: 0,
|
|
140
|
+
}),
|
|
141
|
+
...style,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<motion.div
|
|
146
|
+
ref={ref}
|
|
147
|
+
layout={reduced ? false : layout}
|
|
148
|
+
{...(layer === "end" ? { initial: reduced ? false : { opacity: 0 } } : {})}
|
|
149
|
+
animate={{ opacity: isActive ? 1 : 0 }}
|
|
150
|
+
transition={
|
|
151
|
+
reduced
|
|
152
|
+
? { type: "tween", duration: 0, delay: 0 }
|
|
153
|
+
: transition || getDefaultTransition(isActive, speed)
|
|
154
|
+
}
|
|
155
|
+
style={baseStyle}
|
|
156
|
+
{...props}
|
|
157
|
+
inert={!isActive || props.inert}
|
|
158
|
+
aria-hidden={!isActive || props["aria-hidden"]}
|
|
159
|
+
>
|
|
160
|
+
{children}
|
|
161
|
+
</motion.div>
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* M3ContainerTransformStart component.
|
|
167
|
+
* Represents the starting content (e.g., Card content) inside M3ContainerTransform.
|
|
168
|
+
* Prevents aspect ratio distortion and fades out during the container transform morph.
|
|
169
|
+
*/
|
|
170
|
+
export const M3ContainerTransformStart = React.forwardRef<
|
|
171
|
+
HTMLDivElement,
|
|
172
|
+
M3ContainerTransformStartProps
|
|
173
|
+
>((props, ref) => <ContainerTransformLayer layer="start" ref={ref} {...props} />);
|
|
174
|
+
|
|
175
|
+
M3ContainerTransformStart.displayName = "M3ContainerTransformStart";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* M3ContainerTransformEnd component.
|
|
179
|
+
* Represents the target content (e.g., Detail page content) inside M3ContainerTransform.
|
|
180
|
+
* Prevents aspect ratio distortion and fades in during the container transform morph.
|
|
181
|
+
*/
|
|
182
|
+
export const M3ContainerTransformEnd = React.forwardRef<
|
|
183
|
+
HTMLDivElement,
|
|
184
|
+
M3ContainerTransformEndProps
|
|
185
|
+
>((props, ref) => <ContainerTransformLayer layer="end" ref={ref} {...props} />);
|
|
186
|
+
|
|
187
|
+
M3ContainerTransformEnd.displayName = "M3ContainerTransformEnd";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
3
|
+
import type { HTMLMotionProps } from "motion/react";
|
|
4
|
+
import { m3Easings, m3Durations } from "../easings.js";
|
|
5
|
+
import { motionVariants, useM3ReducedMotion } from "../hooks/useM3ReducedMotion.js";
|
|
6
|
+
|
|
7
|
+
export interface M3FadeProps extends HTMLMotionProps<"div"> {
|
|
8
|
+
/**
|
|
9
|
+
* If true, the component will transition in. If false, it will transition out.
|
|
10
|
+
* If omitted, the component behaves as a standard motion element meant to be unmounted/mounted
|
|
11
|
+
* by an external <AnimatePresence>.
|
|
12
|
+
*/
|
|
13
|
+
in?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* If true, animate the transition on initial mount.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
appear?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* M3Fade transition component.
|
|
23
|
+
* Fades elements in/out using Material 3 standard decelerate/accelerate curves.
|
|
24
|
+
*/
|
|
25
|
+
export const M3Fade = React.forwardRef<HTMLDivElement, M3FadeProps>(
|
|
26
|
+
({ in: inProp, appear = true, children, variants: customVariants, ...props }, ref) => {
|
|
27
|
+
const reduced = useM3ReducedMotion();
|
|
28
|
+
const variants = {
|
|
29
|
+
initial: { opacity: 0 },
|
|
30
|
+
animate: {
|
|
31
|
+
opacity: 1,
|
|
32
|
+
transition: {
|
|
33
|
+
ease: m3Easings.emphasizedDecelerate,
|
|
34
|
+
duration: m3Durations.medium2,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
exit: {
|
|
38
|
+
opacity: 0,
|
|
39
|
+
transition: {
|
|
40
|
+
ease: m3Easings.emphasizedAccelerate,
|
|
41
|
+
duration: m3Durations.short4,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const content = (
|
|
47
|
+
<motion.div
|
|
48
|
+
ref={ref}
|
|
49
|
+
variants={motionVariants(customVariants ?? variants, reduced)}
|
|
50
|
+
initial={appear && !reduced ? "initial" : false}
|
|
51
|
+
animate="animate"
|
|
52
|
+
exit="exit"
|
|
53
|
+
style={{ willChange: "opacity" }}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</motion.div>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (inProp !== undefined) {
|
|
61
|
+
return <AnimatePresence>{inProp ? content : null}</AnimatePresence>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return content;
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
M3Fade.displayName = "M3Fade";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
3
|
+
import type { HTMLMotionProps, Transition, Variants } from "motion/react";
|
|
4
|
+
import { m3Easings } from "../easings.js";
|
|
5
|
+
import { motionVariants, useM3ReducedMotion } from "../hooks/useM3ReducedMotion.js";
|
|
6
|
+
|
|
7
|
+
export interface M3FadeThroughProps extends HTMLMotionProps<"div"> {
|
|
8
|
+
/**
|
|
9
|
+
* Unique key representing the active view/content. Changing this key triggers the transition.
|
|
10
|
+
*/
|
|
11
|
+
stateKey: React.Key;
|
|
12
|
+
/**
|
|
13
|
+
* Custom transition for the fade through animation.
|
|
14
|
+
*/
|
|
15
|
+
transition?: Transition;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* M3FadeThrough transition component.
|
|
20
|
+
* Implements the Material 3 Fade Through navigation pattern.
|
|
21
|
+
* The outgoing element fades out over 90ms, followed by the incoming element fading in and scaling up over 210ms.
|
|
22
|
+
*/
|
|
23
|
+
export const M3FadeThrough: React.FC<M3FadeThroughProps> = ({
|
|
24
|
+
stateKey,
|
|
25
|
+
transition: customTransition,
|
|
26
|
+
variants: customVariants,
|
|
27
|
+
children,
|
|
28
|
+
...props
|
|
29
|
+
}) => {
|
|
30
|
+
const reduced = useM3ReducedMotion();
|
|
31
|
+
const defaultVariants: Variants = {
|
|
32
|
+
initial: {
|
|
33
|
+
opacity: 0,
|
|
34
|
+
scale: 0.92,
|
|
35
|
+
},
|
|
36
|
+
animate: {
|
|
37
|
+
opacity: 1,
|
|
38
|
+
scale: 1,
|
|
39
|
+
transition: customTransition || {
|
|
40
|
+
opacity: {
|
|
41
|
+
ease: m3Easings.emphasizedDecelerate,
|
|
42
|
+
duration: 0.21,
|
|
43
|
+
},
|
|
44
|
+
scale: {
|
|
45
|
+
ease: m3Easings.emphasizedDecelerate,
|
|
46
|
+
duration: 0.21,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
exit: {
|
|
51
|
+
opacity: 0,
|
|
52
|
+
transition: customTransition || {
|
|
53
|
+
opacity: {
|
|
54
|
+
ease: m3Easings.emphasizedAccelerate,
|
|
55
|
+
duration: 0.09,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const resolvedVariants = motionVariants(customVariants ?? defaultVariants, reduced);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<AnimatePresence mode="wait">
|
|
65
|
+
<motion.div
|
|
66
|
+
key={stateKey}
|
|
67
|
+
variants={resolvedVariants}
|
|
68
|
+
initial={reduced ? false : "initial"}
|
|
69
|
+
animate="animate"
|
|
70
|
+
exit="exit"
|
|
71
|
+
style={{ willChange: "opacity, transform" }}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</motion.div>
|
|
76
|
+
</AnimatePresence>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
M3FadeThrough.displayName = "M3FadeThrough";
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { AnimatePresence, motion, useIsPresent } from "motion/react";
|
|
4
|
+
import type { HTMLMotionProps, Variants } from "motion/react";
|
|
5
|
+
import { m3Easings, m3Durations } from "../easings.js";
|
|
6
|
+
import { motionVariants, useM3ReducedMotion } from "../hooks/useM3ReducedMotion.js";
|
|
7
|
+
|
|
8
|
+
export interface M3ListStaggerProps {
|
|
9
|
+
/**
|
|
10
|
+
* Delay between each child item entering the scene (seconds).
|
|
11
|
+
* @default 0.05
|
|
12
|
+
*/
|
|
13
|
+
staggerDelay?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Delay before the stagger sequence starts (seconds).
|
|
16
|
+
* @default 0
|
|
17
|
+
*/
|
|
18
|
+
delayChildren?: number;
|
|
19
|
+
/**
|
|
20
|
+
* If true, items animate on initial render.
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
appear?: boolean;
|
|
24
|
+
className?: string;
|
|
25
|
+
style?: React.CSSProperties;
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface M3ListStaggerItemProps extends HTMLMotionProps<"div"> {
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const containerVariants: Variants = {
|
|
34
|
+
hidden: {},
|
|
35
|
+
visible: (_custom: { staggerDelay: number; delayChildren: number }) => ({
|
|
36
|
+
transition: {
|
|
37
|
+
staggerChildren: _custom.staggerDelay,
|
|
38
|
+
delayChildren: _custom.delayChildren,
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
exit: {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const itemVariants: Variants = {
|
|
45
|
+
hidden: { opacity: 0, y: 8 },
|
|
46
|
+
visible: {
|
|
47
|
+
opacity: 1,
|
|
48
|
+
y: 0,
|
|
49
|
+
transition: {
|
|
50
|
+
ease: m3Easings.emphasizedDecelerate,
|
|
51
|
+
duration: m3Durations.medium2,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
exit: {
|
|
55
|
+
opacity: 0,
|
|
56
|
+
y: 4,
|
|
57
|
+
transition: {
|
|
58
|
+
ease: m3Easings.emphasizedAccelerate,
|
|
59
|
+
duration: m3Durations.short3,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* M3ListStagger — stagger container for list-item enter/exit animations.
|
|
66
|
+
*
|
|
67
|
+
* Usage:
|
|
68
|
+
* <M3ListStagger>
|
|
69
|
+
* {items.map(item => <M3ListStaggerItem key={item.id}>{item.label}</M3ListStaggerItem>)}
|
|
70
|
+
* </M3ListStagger>
|
|
71
|
+
*/
|
|
72
|
+
export const M3ListStagger = React.forwardRef<HTMLDivElement, M3ListStaggerProps>(
|
|
73
|
+
({ staggerDelay = 0.05, delayChildren = 0, appear = true, children, className, style }, ref) => {
|
|
74
|
+
const reduced = useM3ReducedMotion();
|
|
75
|
+
return (
|
|
76
|
+
<motion.div
|
|
77
|
+
ref={ref}
|
|
78
|
+
className={className}
|
|
79
|
+
style={style}
|
|
80
|
+
variants={motionVariants(containerVariants, reduced)}
|
|
81
|
+
initial={appear && !reduced ? "hidden" : false}
|
|
82
|
+
animate="visible"
|
|
83
|
+
exit="exit"
|
|
84
|
+
custom={{ staggerDelay, delayChildren }}
|
|
85
|
+
>
|
|
86
|
+
<AnimatePresence initial={appear && !reduced}>{children}</AnimatePresence>
|
|
87
|
+
</motion.div>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
M3ListStagger.displayName = "M3ListStagger";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* M3ListStaggerItem — a single animated item inside M3ListStagger.
|
|
96
|
+
* Must be a direct or indirect descendant of M3ListStagger to inherit variants.
|
|
97
|
+
*/
|
|
98
|
+
export const M3ListStaggerItem = React.forwardRef<HTMLDivElement, M3ListStaggerItemProps>(
|
|
99
|
+
({ children, style, ...props }, ref) => {
|
|
100
|
+
const present = useIsPresent();
|
|
101
|
+
const reduced = useM3ReducedMotion();
|
|
102
|
+
return (
|
|
103
|
+
<motion.div
|
|
104
|
+
ref={ref}
|
|
105
|
+
variants={motionVariants(itemVariants, reduced)}
|
|
106
|
+
style={{ willChange: "opacity, transform", ...style }}
|
|
107
|
+
{...props}
|
|
108
|
+
inert={!present || props.inert}
|
|
109
|
+
aria-hidden={!present || props["aria-hidden"]}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</motion.div>
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
M3ListStaggerItem.displayName = "M3ListStaggerItem";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
3
|
+
import type { HTMLMotionProps, Transition, Variants } from "motion/react";
|
|
4
|
+
import { m3Easings, m3Durations } from "../easings.js";
|
|
5
|
+
import { motionVariants, useM3ReducedMotion } from "../hooks/useM3ReducedMotion.js";
|
|
6
|
+
|
|
7
|
+
export interface M3SharedAxisProps extends HTMLMotionProps<"div"> {
|
|
8
|
+
/**
|
|
9
|
+
* If true, the component will transition in. If false, it will transition out.
|
|
10
|
+
* If omitted, the component behaves as a standard motion element meant to be unmounted/mounted
|
|
11
|
+
* by an external <AnimatePresence>.
|
|
12
|
+
*/
|
|
13
|
+
in?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Axis of translation.
|
|
16
|
+
* @default "x"
|
|
17
|
+
*/
|
|
18
|
+
axis?: "x" | "y" | "z";
|
|
19
|
+
/**
|
|
20
|
+
* Direction of navigation. If true, slides forward/up/zoom-in. If false, slides backward/down/zoom-out.
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
forward?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* If true, animate the transition on initial mount.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
appear?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Custom transition for the shared axis animation.
|
|
31
|
+
*/
|
|
32
|
+
transition?: Transition;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* M3SharedAxis transition component.
|
|
37
|
+
* Implements the Material 3 Shared Axis motion pattern (X, Y, Z axes).
|
|
38
|
+
*/
|
|
39
|
+
export const M3SharedAxis = React.forwardRef<HTMLDivElement, M3SharedAxisProps>(
|
|
40
|
+
(
|
|
41
|
+
{
|
|
42
|
+
in: inProp,
|
|
43
|
+
axis = "x",
|
|
44
|
+
forward = true,
|
|
45
|
+
appear = true,
|
|
46
|
+
transition: customTransition,
|
|
47
|
+
variants: customVariants,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
},
|
|
51
|
+
ref,
|
|
52
|
+
) => {
|
|
53
|
+
const reduced = useM3ReducedMotion();
|
|
54
|
+
// Offset values according to M3 specifications (usually 30px translation or ~8% scale change)
|
|
55
|
+
const translateOffset = 30;
|
|
56
|
+
const scaleOffset = 0.08;
|
|
57
|
+
|
|
58
|
+
const defaultVariants: Variants = {
|
|
59
|
+
initial: {
|
|
60
|
+
opacity: 0,
|
|
61
|
+
x: axis === "x" ? (forward ? translateOffset : -translateOffset) : 0,
|
|
62
|
+
y: axis === "y" ? (forward ? translateOffset : -translateOffset) : 0,
|
|
63
|
+
scale: axis === "z" ? (forward ? 1 - scaleOffset : 1 + scaleOffset) : 1,
|
|
64
|
+
},
|
|
65
|
+
animate: {
|
|
66
|
+
opacity: 1,
|
|
67
|
+
x: 0,
|
|
68
|
+
y: 0,
|
|
69
|
+
scale: 1,
|
|
70
|
+
transition: customTransition || {
|
|
71
|
+
x: { ease: m3Easings.emphasizedDecelerate, duration: m3Durations.medium2 },
|
|
72
|
+
y: { ease: m3Easings.emphasizedDecelerate, duration: m3Durations.medium2 },
|
|
73
|
+
scale: { ease: m3Easings.emphasizedDecelerate, duration: m3Durations.medium2 },
|
|
74
|
+
opacity: { ease: m3Easings.standard, duration: m3Durations.medium2 },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
exit: {
|
|
78
|
+
opacity: 0,
|
|
79
|
+
x: axis === "x" ? (forward ? -translateOffset : translateOffset) : 0,
|
|
80
|
+
y: axis === "y" ? (forward ? -translateOffset : translateOffset) : 0,
|
|
81
|
+
scale: axis === "z" ? (forward ? 1 + scaleOffset : 1 - scaleOffset) : 1,
|
|
82
|
+
transition: customTransition || {
|
|
83
|
+
x: { ease: m3Easings.emphasizedAccelerate, duration: m3Durations.short4 },
|
|
84
|
+
y: { ease: m3Easings.emphasizedAccelerate, duration: m3Durations.short4 },
|
|
85
|
+
scale: { ease: m3Easings.emphasizedAccelerate, duration: m3Durations.short4 },
|
|
86
|
+
opacity: { ease: m3Easings.standard, duration: m3Durations.short4 },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const resolvedVariants = motionVariants(customVariants ?? defaultVariants, reduced);
|
|
92
|
+
|
|
93
|
+
const content = (
|
|
94
|
+
<motion.div
|
|
95
|
+
ref={ref}
|
|
96
|
+
variants={resolvedVariants}
|
|
97
|
+
initial={appear && !reduced ? "initial" : false}
|
|
98
|
+
animate="animate"
|
|
99
|
+
exit="exit"
|
|
100
|
+
style={{ willChange: "opacity, transform" }}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</motion.div>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (inProp !== undefined) {
|
|
108
|
+
return <AnimatePresence mode="wait">{inProp ? content : null}</AnimatePresence>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return content;
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
M3SharedAxis.displayName = "M3SharedAxis";
|