Skip to content

Commit 61fcc82

Browse files
committed
broke projects into separate components and page, and creating new system for animations using scroll instead of viewport
1 parent 1faa627 commit 61fcc82

15 files changed

+1053
-231
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Apps from "./components/pages/apps";
1010
import AppDetails from "./components/pages/app-details";
1111
import { Footer } from "./components/footer";
1212
import Policy from "./components/pages/policy";
13+
import ProjectsPage from "./components/pages/projects-page";
1314

1415
function Root() {
1516
const { state } = useLocation();
@@ -37,6 +38,7 @@ function RootLayout() {
3738
<main className="py-20">
3839
<Routes>
3940
<Route path="/" element={<Root />} />
41+
<Route path="/projects" element={<ProjectsPage />} />
4042
<Route path="/projects/:id" element={<ProjectDetails />} />
4143
<Route path="/apps" element={<Apps />} />
4244
<Route path="/apps/:id">

src/components/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function Header() {
124124
</Tooltip>
125125

126126
<Tooltip>
127-
<TooltipTrigger>
127+
<TooltipTrigger asChild>
128128
<ModeToggle />
129129
</TooltipTrigger>
130130
<TooltipContent>Toggle Theme</TooltipContent>

src/components/mode-toggle.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Moon, Sun } from "lucide-react";
33
import { Button } from "@/components/ui/button";
44
import { useTheme } from "@/components/theme-provider";
55

6-
export function ModeToggle() {
6+
export function ModeToggle({ ...props }: React.ComponentProps<typeof Button>) {
77
const { theme, setTheme } = useTheme();
88

99
const handleToggle = () => {
@@ -21,7 +21,7 @@ export function ModeToggle() {
2121
};
2222

2323
return (
24-
<Button variant="ghost" size="icon" onClick={handleToggle}>
24+
<Button {...props} variant="ghost" size="icon" onClick={handleToggle}>
2525
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
2626
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
2727
<span className="sr-only">Toggle theme</span>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { motion } from "framer-motion";
2+
import type { Variants } from "framer-motion";
3+
4+
interface AnimateOnThresholdProps
5+
extends Omit<React.ComponentProps<typeof motion.div>, "animate" | "initial"> {
6+
/** Whether the threshold has been passed - controls animation */
7+
shouldAnimate: boolean;
8+
/** Delay before animation starts (in seconds) */
9+
delay?: number;
10+
/** Custom animation variants */
11+
variants?: Variants;
12+
/** Custom initial state (object with animation properties) */
13+
initial?: React.ComponentProps<typeof motion.div>["initial"];
14+
/** Custom animate state (object with animation properties) */
15+
animate?: React.ComponentProps<typeof motion.div>["animate"];
16+
/** Transition configuration */
17+
transition?: React.ComponentProps<typeof motion.div>["transition"];
18+
}
19+
20+
/**
21+
* AnimateOnThreshold - Simple animation component that animates based on a boolean flag
22+
*
23+
* This component is designed to work with useScrollThreshold - when the threshold
24+
* is passed, it animates children with a configurable delay. Perfect for sequential
25+
* animations within a container.
26+
*
27+
* @example
28+
* const ref = useRef<HTMLDivElement>(null);
29+
* const isPast = useScrollThreshold(ref, 0.2);
30+
*
31+
* <div ref={ref}>
32+
* <AnimateOnThreshold shouldAnimate={isPast} delay={0}>
33+
* First item
34+
* </AnimateOnThreshold>
35+
* <AnimateOnThreshold shouldAnimate={isPast} delay={0.1}>
36+
* Second item
37+
* </AnimateOnThreshold>
38+
* </div>
39+
*/
40+
export const AnimateOnThreshold = ({
41+
children,
42+
shouldAnimate,
43+
delay = 0,
44+
variants,
45+
initial: customInitial,
46+
animate: customAnimate,
47+
transition = { duration: 0.5 },
48+
...props
49+
}: AnimateOnThresholdProps) => {
50+
// If custom variants provided, use them; otherwise create from initial/animate props
51+
// Add transition variants: entrance uses delay, exit uses 0 delay
52+
const defaultInitial =
53+
typeof customInitial === "object" && customInitial !== null
54+
? customInitial
55+
: { opacity: 0, y: 30 };
56+
const defaultAnimate =
57+
typeof customAnimate === "object" && customAnimate !== null
58+
? customAnimate
59+
: { opacity: 1, y: 0 };
60+
61+
const finalVariants: Variants = variants
62+
? variants
63+
: ({
64+
hidden: {
65+
...defaultInitial,
66+
transition: { ...transition, delay: 0 },
67+
},
68+
visible: {
69+
...defaultAnimate,
70+
transition: { ...transition, delay },
71+
},
72+
} as Variants);
73+
74+
return (
75+
<motion.div
76+
{...props}
77+
variants={finalVariants}
78+
initial="hidden"
79+
animate={shouldAnimate ? "visible" : "hidden"}
80+
>
81+
{children}
82+
</motion.div>
83+
);
84+
};
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { StickyParallaxSection } from "@/components/motion/sticky-parallax-section";
2+
import { motion } from "framer-motion";
3+
4+
/**
5+
* ParallaxDemo - Demonstration of multiple stacked StickyParallaxSection components
6+
*
7+
* This demo shows how multiple scroll-driven parallax sections work together
8+
* to create a continuous, immersive scrolling experience.
9+
*
10+
* Performance Notes:
11+
* - Each section independently tracks its own scroll progress
12+
* - Framer Motion's motion values update without triggering React re-renders
13+
* - useScroll efficiently shares a single scroll listener across all sections
14+
* - 10-20 sections will maintain 60fps on modern devices
15+
*
16+
* How it works:
17+
* 1. Each section uses useScroll with its own ref to track scroll progress
18+
* 2. useTransform creates derived animation values from scroll progress
19+
* 3. Motion values update directly (no React state changes)
20+
* 4. Parallax is achieved by different transform speeds per layer
21+
*/
22+
const ParallaxDemo = () => {
23+
return (
24+
<div className="relative">
25+
{/* Hero/Intro Section */}
26+
<div className="h-screen flex items-center justify-center bg-background">
27+
<div className="text-center space-y-6 px-6">
28+
<motion.h1
29+
initial={{ opacity: 0, y: 20 }}
30+
animate={{ opacity: 1, y: 0 }}
31+
transition={{ duration: 0.8 }}
32+
className="text-6xl md:text-8xl font-bold"
33+
>
34+
Scroll-Driven Parallax
35+
</motion.h1>
36+
<motion.p
37+
initial={{ opacity: 0, y: 20 }}
38+
animate={{ opacity: 1, y: 0 }}
39+
transition={{ duration: 0.8, delay: 0.2 }}
40+
className="text-xl text-muted-foreground max-w-2xl mx-auto"
41+
>
42+
Scroll down to experience smooth, performant scroll-driven
43+
animations with multiple parallax layers
44+
</motion.p>
45+
<motion.div
46+
initial={{ opacity: 0 }}
47+
animate={{ opacity: 1 }}
48+
transition={{ duration: 1, delay: 0.5 }}
49+
className="flex flex-col items-center gap-2 mt-8"
50+
>
51+
<span className="text-sm text-muted-foreground">
52+
Scroll to explore
53+
</span>
54+
<motion.div
55+
animate={{ y: [0, 10, 0] }}
56+
transition={{ duration: 1.5, repeat: Infinity }}
57+
className="text-primary"
58+
>
59+
60+
</motion.div>
61+
</motion.div>
62+
</div>
63+
</div>
64+
65+
{/* Parallax Section 1 - Technology Focus */}
66+
<StickyParallaxSection
67+
title="Innovation"
68+
content="Experience the power of scroll-driven animations. Each layer moves at a different speed, creating depth and immersion."
69+
backgroundColor="bg-slate-950 text-white"
70+
/>
71+
72+
{/* Parallax Section 2 - Design Focus */}
73+
<StickyParallaxSection
74+
title="Design"
75+
content="Beautiful parallax effects that respond to your scroll. Watch how the background, midground, and foreground layers create a sense of depth."
76+
backgroundColor="bg-blue-950 text-white"
77+
/>
78+
79+
{/* Parallax Section 3 - Custom Children Example */}
80+
<StickyParallaxSection backgroundColor="bg-purple-950 text-white">
81+
<div className="text-center space-y-8 px-6">
82+
<h2 className="text-5xl md:text-7xl font-bold">Performance</h2>
83+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl mx-auto">
84+
<motion.div
85+
initial={{ opacity: 0, y: 20 }}
86+
whileInView={{ opacity: 1, y: 0 }}
87+
transition={{ duration: 0.5 }}
88+
className="bg-white/10 backdrop-blur-sm rounded-lg p-6"
89+
>
90+
<div className="text-4xl font-bold text-primary">60fps</div>
91+
<div className="text-sm text-muted-foreground mt-2">
92+
Smooth animations
93+
</div>
94+
</motion.div>
95+
<motion.div
96+
initial={{ opacity: 0, y: 20 }}
97+
whileInView={{ opacity: 1, y: 0 }}
98+
transition={{ duration: 0.5, delay: 0.1 }}
99+
className="bg-white/10 backdrop-blur-sm rounded-lg p-6"
100+
>
101+
<div className="text-4xl font-bold text-primary">0</div>
102+
<div className="text-sm text-muted-foreground mt-2">
103+
Re-renders on scroll
104+
</div>
105+
</motion.div>
106+
<motion.div
107+
initial={{ opacity: 0, y: 20 }}
108+
whileInView={{ opacity: 1, y: 0 }}
109+
transition={{ duration: 0.5, delay: 0.2 }}
110+
className="bg-white/10 backdrop-blur-sm rounded-lg p-6"
111+
>
112+
<div className="text-4xl font-bold text-primary">10-20</div>
113+
<div className="text-sm text-muted-foreground mt-2">
114+
Sections supported
115+
</div>
116+
</motion.div>
117+
</div>
118+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
119+
Built with Framer Motion's motion values for maximum performance.
120+
Each section independently tracks scroll progress without impacting
121+
others.
122+
</p>
123+
</div>
124+
</StickyParallaxSection>
125+
126+
{/* Parallax Section 4 - Experience Focus */}
127+
<StickyParallaxSection
128+
title="Experience"
129+
content="As you scroll through each section, notice how different layers move at different speeds. This creates a natural sense of depth and makes the content feel alive."
130+
backgroundColor="bg-indigo-950 text-white"
131+
/>
132+
133+
{/* Parallax Section 5 - Technology Stack */}
134+
<StickyParallaxSection backgroundColor="bg-emerald-950 text-white">
135+
<div className="text-center space-y-8 px-6">
136+
<h2 className="text-5xl md:text-7xl font-bold">Built With</h2>
137+
<div className="flex flex-wrap justify-center gap-4 max-w-3xl mx-auto">
138+
{[
139+
"React 19",
140+
"TypeScript",
141+
"Framer Motion",
142+
"useScroll",
143+
"useTransform",
144+
"Motion Values",
145+
].map((tech, index) => (
146+
<motion.div
147+
key={tech}
148+
initial={{ opacity: 0, scale: 0.8 }}
149+
whileInView={{ opacity: 1, scale: 1 }}
150+
transition={{ duration: 0.3, delay: index * 0.1 }}
151+
className="bg-white/10 backdrop-blur-sm rounded-full px-6 py-3 text-sm font-medium"
152+
>
153+
{tech}
154+
</motion.div>
155+
))}
156+
</div>
157+
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mt-8">
158+
Leveraging the latest React and Framer Motion features for
159+
buttery-smooth scroll-driven animations.
160+
</p>
161+
</div>
162+
</StickyParallaxSection>
163+
164+
{/* Final Section - Call to Action */}
165+
<StickyParallaxSection
166+
title="Keep Scrolling"
167+
content="Each section is completely independent and reusable. Add as many as you need - the performance stays smooth thanks to Framer Motion's optimized architecture."
168+
backgroundColor="bg-slate-900 text-white"
169+
/>
170+
171+
{/* End Section */}
172+
<div className="h-screen flex items-center justify-center bg-background">
173+
<div className="text-center space-y-6 px-6">
174+
<motion.h2
175+
initial={{ opacity: 0, y: 20 }}
176+
whileInView={{ opacity: 1, y: 0 }}
177+
transition={{ duration: 0.8 }}
178+
className="text-5xl md:text-7xl font-bold"
179+
>
180+
The End
181+
</motion.h2>
182+
<motion.p
183+
initial={{ opacity: 0, y: 20 }}
184+
whileInView={{ opacity: 1, y: 0 }}
185+
transition={{ duration: 0.8, delay: 0.2 }}
186+
className="text-xl text-muted-foreground max-w-2xl mx-auto"
187+
>
188+
Scroll back up to see the animations in reverse!
189+
</motion.p>
190+
<motion.div
191+
initial={{ opacity: 0 }}
192+
whileInView={{ opacity: 1 }}
193+
transition={{ duration: 1, delay: 0.5 }}
194+
className="flex flex-col items-center gap-2 mt-8"
195+
>
196+
<motion.div
197+
animate={{ y: [0, -10, 0] }}
198+
transition={{ duration: 1.5, repeat: Infinity }}
199+
className="text-primary"
200+
>
201+
202+
</motion.div>
203+
<span className="text-sm text-muted-foreground">
204+
Scroll back up
205+
</span>
206+
</motion.div>
207+
</div>
208+
</div>
209+
</div>
210+
);
211+
};
212+
213+
export default ParallaxDemo;

0 commit comments

Comments
 (0)