Add scroll-reveal animations across home, services, portfolio, about, apps

New ScrollReveal client component wraps section-containers and cards with
a subtle opacity + y-translate fade-in triggered by IntersectionObserver.
Bails out to an unanimated render when prefers-reduced-motion is set.

- home: Selected Works, Inclusive by Design, Start a Project
- services: each of the four verticals and the Recent Work card
- portfolio: each product card, slight stagger by column index
- about: Leadership section and Get in Touch
- apps: each app card, stagger by index
This commit is contained in:
jwinterm
2026-04-20 13:28:03 -04:00
parent 7eb531170d
commit 5c1e3d597f
6 changed files with 160 additions and 72 deletions
+77
View File
@@ -0,0 +1,77 @@
'use client';
import { motion, useReducedMotion, type Variants } from 'framer-motion';
import { createElement, type ReactNode } from 'react';
type Direction = 'up' | 'down' | 'left' | 'right' | 'none';
type Tag = 'div' | 'section' | 'article' | 'li';
interface ScrollRevealProps {
children: ReactNode;
direction?: Direction;
delay?: number;
duration?: number;
distance?: number;
amount?: number;
className?: string;
as?: Tag;
}
const axisFor = (direction: Direction, distance: number) => {
switch (direction) {
case 'up': return { x: 0, y: distance };
case 'down': return { x: 0, y: -distance };
case 'left': return { x: distance, y: 0 };
case 'right': return { x: -distance, y: 0 };
case 'none': return { x: 0, y: 0 };
}
};
export function ScrollReveal({
children,
direction = 'up',
delay = 0,
duration = 0.6,
distance = 24,
amount = 0.2,
className,
as = 'div',
}: ScrollRevealProps) {
const prefersReducedMotion = useReducedMotion();
if (prefersReducedMotion) {
return createElement(as, { className }, children);
}
const offset = axisFor(direction, distance);
const variants: Variants = {
hidden: { opacity: 0, ...offset },
visible: {
opacity: 1,
x: 0,
y: 0,
transition: {
duration,
delay,
ease: [0.16, 1, 0.3, 1],
},
},
};
const common = {
className,
initial: 'hidden',
whileInView: 'visible',
viewport: { once: true, amount },
variants,
children,
};
const Component =
as === 'section' ? motion.section :
as === 'article' ? motion.article :
as === 'li' ? motion.li :
motion.div;
return createElement(Component as never, common);
}