Skip to content

Commit e63ec73

Browse files
committed
feat: enhance ModeToggle component with animations and integrate into Home page
1 parent ce51e2e commit e63ec73

File tree

2 files changed

+155
-36
lines changed

2 files changed

+155
-36
lines changed

src/components/animations/mode-toggle.tsx

Lines changed: 152 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,178 @@ import {
44
MotionConfig,
55
Variants,
66
} from "motion/react";
7+
import { useTheme } from "@/providers/theme";
78
import { useState } from "react";
9+
810
const modeVariants: Variants = {
911
initial: {
10-
y: -40,
12+
y: -20,
1113
opacity: 0,
1214
},
1315
visible: {
1416
y: 0,
1517
opacity: 1,
18+
transition: {
19+
type: "spring",
20+
stiffness: 300,
21+
damping: 20,
22+
duration: 0.3,
23+
},
1624
},
1725
hidden: {
18-
y: 40,
26+
y: 20,
1927
opacity: 0,
28+
transition: {
29+
duration: 0.3,
30+
},
31+
},
32+
};
33+
34+
const containerVariants: Variants = {
35+
hover: {
36+
scale: 1.05,
37+
},
38+
tap: {
39+
scale: 0.95,
2040
},
2141
};
2242

2343
const ModeToggle = () => {
24-
const [mode, setMode] = useState<"light" | "dark">("light");
44+
const { theme, setTheme } = useTheme();
45+
const [isAnimating, setIsAnimating] = useState(false);
46+
47+
const handleToggle = () => {
48+
setIsAnimating(true);
49+
setTheme(theme === "light" ? "dark" : "light");
50+
// Reset animation state after animation completes
51+
setTimeout(() => setIsAnimating(false), 600);
52+
};
2553

2654
return (
27-
<MotionConfig transition={{ duration: 0.3, ease: "easeInOut" }}>
55+
<MotionConfig
56+
transition={{
57+
easings: [0.1, 0.9, 0.2, 1],
58+
duration: 0.5,
59+
}}
60+
>
2861
<div className="full center">
2962
<m.button
30-
animate={{ width: "auto" }}
31-
whileTap={{ scale: 0.95 }}
32-
className="rounded-full bg-muted flex items-center justify-center gap-1 py-2 px-4 overflow-hidden "
33-
onClick={() => setMode(mode === "light" ? "dark" : "light")}
63+
variants={containerVariants}
64+
whileHover="hover"
65+
whileTap="tap"
66+
animate={{
67+
width: "auto",
68+
}}
69+
className="rounded-full bg-muted flex items-center justify-center py-2 px-4 overflow-hidden outline-none relative"
70+
onClick={handleToggle}
3471
>
35-
<div className="size-5 rounded bg-muted-foreground"></div>
36-
<AnimatePresence mode="wait">
37-
{mode === "light" ? (
38-
<m.span
39-
variants={modeVariants}
40-
initial="initial"
41-
animate="visible"
42-
exit="hidden"
43-
key="light"
44-
>
45-
Light
46-
</m.span>
47-
) : (
48-
<m.span
49-
variants={modeVariants}
50-
initial="initial"
51-
animate="visible"
52-
exit="hidden"
53-
key="dark"
54-
>
55-
Dark
56-
</m.span>
57-
)}
58-
</AnimatePresence>
59-
<span>Mode</span>
72+
<m.div className="size-10 rounded-full center relative" layout>
73+
<m.svg
74+
xmlns="http://www.w3.org/2000/svg"
75+
width="100"
76+
height="100"
77+
viewBox="0 0 100 100"
78+
fill="none"
79+
className="size-9 will-change-transform"
80+
animate={{
81+
rotate: theme === "light" ? 0 : 180,
82+
scale: isAnimating ? [1, 1.1, 1] : 1,
83+
}}
84+
transition={{
85+
duration: 0.5,
86+
}}
87+
>
88+
<rect width="100" height="100"></rect>
89+
<m.path
90+
d="M50 18C58.4869 18 66.6262 21.3714 72.6274 27.3726C78.6286 33.3737 82 41.513 82 50C82 58.4869 78.6286 66.6262 72.6275 72.6274C66.6263 78.6286 58.487 82 50.0001 82L50 50L50 18Z"
91+
className="will-change-transform fill-primary"
92+
animate={{
93+
fillOpacity: isAnimating ? [1, 0.7, 1] : 1,
94+
}}
95+
transition={{
96+
duration: 0.3,
97+
times: [0, 0.5, 1],
98+
}}
99+
></m.path>
100+
<m.circle
101+
cx="50"
102+
cy="50"
103+
r="30"
104+
className="will-change-transform stroke-primary"
105+
strokeWidth="4"
106+
animate={{
107+
strokeWidth: isAnimating ? [4, 5, 4] : 4,
108+
}}
109+
transition={{
110+
duration: 0.5,
111+
times: [0, 0.5, 1],
112+
}}
113+
></m.circle>
114+
<m.circle
115+
cx="50"
116+
cy="50"
117+
r="12"
118+
className="will-change-transform fill-primary"
119+
animate={{
120+
scale: isAnimating ? [1, 0.9, 1] : 1,
121+
}}
122+
transition={{
123+
duration: 0.5,
124+
times: [0, 0.5, 1],
125+
}}
126+
></m.circle>
127+
<m.path
128+
d="M50 62C53.1826 62 56.2348 60.7357 58.4853 58.4853C60.7357 56.2348 62 53.1826 62 50C62 46.8174 60.7357 43.7652 58.4853 41.5147C56.2348 39.2643 53.1826 38 50 38L50 50L50 62Z"
129+
className="will-change-transform fill-primary-foreground"
130+
animate={{
131+
fillOpacity: isAnimating ? [1, 0.7, 1] : 1,
132+
}}
133+
transition={{
134+
duration: 0.5,
135+
times: [0, 0.5, 1],
136+
}}
137+
></m.path>
138+
</m.svg>
139+
</m.div>
140+
141+
<div className="flex items-center gap-1 font-medium">
142+
<AnimatePresence mode="wait">
143+
{theme === "light" ? (
144+
<m.span
145+
variants={modeVariants}
146+
initial="initial"
147+
animate="visible"
148+
exit="hidden"
149+
key="light"
150+
className="font-medium text-primary"
151+
>
152+
Light
153+
</m.span>
154+
) : (
155+
<m.span
156+
variants={modeVariants}
157+
initial="initial"
158+
animate="visible"
159+
exit="hidden"
160+
key="dark"
161+
className="font-medium text-primary"
162+
>
163+
Dark
164+
</m.span>
165+
)}
166+
</AnimatePresence>
167+
<m.span
168+
animate={{
169+
opacity: isAnimating ? [1, 0.7, 1] : 1,
170+
}}
171+
transition={{
172+
duration: 0.3,
173+
times: [0, 0.5, 1],
174+
}}
175+
>
176+
Mode
177+
</m.span>
178+
</div>
60179
</m.button>
61180
</div>
62181
</MotionConfig>

src/pages/home.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,15 @@ const Home = () => {
7575
<div className="h-screen fixed top-0 max-w-screen-lg inset-x-0 w-full mx-auto border-x border -z-10 candy-bg hidden md:block" />
7676
<Hero hidden={isHidden} />
7777
<Feedback hidden={isHidden} />
78+
<ComponentPreview>
79+
<ModeToggle />
80+
</ComponentPreview>
7881
<ComponentPreview notReady>
7982
<Volume />
8083
</ComponentPreview>
8184
<ComponentPreview notReady>
8285
<Map />
8386
</ComponentPreview>
84-
<ComponentPreview notReady>
85-
<ModeToggle />
86-
</ComponentPreview>
8787
<ComponentPreview>
8888
<View />
8989
</ComponentPreview>

0 commit comments

Comments
 (0)