agency-web/app/components/Reveal.tsx

129 lines
3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* In-view reveal — the page's core entrance vocabulary.
* - Default: lift + fade + a touch of blur(4px -> 0) so content resolves INTO
* place instead of just sliding (Emil's blur-masked entrance). Never appears
* from nothing.
* - variant="clip": a premium clip-path inset() wipe (bottom -> full) for
* section/media reveals, paired with a small lift.
* - `stagger` cascades direct <RevealItem> children via variants (3080ms).
* - Custom EASE_OUT curve everywhere; honors prefers-reduced-motion (keeps
* opacity/color, drops all movement + blur).
*/
import { motion, type Variants, useReducedMotion } from "motion/react";
import { EASE_OUT } from "./motion";
type Props = {
children: React.ReactNode;
className?: string;
delay?: number;
y?: number;
stagger?: number;
variant?: "lift" | "clip";
as?:
| "div"
| "section"
| "ul"
| "ol"
| "li"
| "p"
| "figure"
| "header"
| "span"
| "h2";
};
const VIEWPORT = { once: true, margin: "0px 0px -12% 0px" } as const;
export default function Reveal({
children,
className,
delay = 0,
y = 28,
stagger,
variant = "lift",
as = "div",
}: Props) {
const reduce = useReducedMotion();
const MotionTag = motion[as] as typeof motion.div;
if (stagger) {
const parent: Variants = {
hidden: {},
show: {
transition: {
staggerChildren: reduce ? 0 : stagger,
delayChildren: delay / 1000,
},
},
};
return (
<MotionTag
className={className}
variants={parent}
initial="hidden"
whileInView="show"
viewport={VIEWPORT}
>
{children}
</MotionTag>
);
}
const hidden =
variant === "clip"
? reduce
? { opacity: 1 }
: { opacity: 0, y: y * 0.5, clipPath: "inset(0 0 100% 0)" }
: reduce
? { opacity: 1 }
: { opacity: 0, y, filter: "blur(4px)" };
const shown =
variant === "clip"
? { opacity: 1, y: 0, clipPath: "inset(0 0 0% 0)" }
: { opacity: 1, y: 0, filter: "blur(0px)" };
return (
<MotionTag
className={className}
initial={hidden}
whileInView={shown}
viewport={VIEWPORT}
transition={{ duration: 0.9, delay: delay / 1000, ease: EASE_OUT }}
>
{children}
</MotionTag>
);
}
/** A child item that participates in a parent <Reveal stagger>. */
export function RevealItem({
children,
className,
y = 24,
as = "div",
}: {
children: React.ReactNode;
className?: string;
y?: number;
as?: "div" | "li" | "p";
}) {
const reduce = useReducedMotion();
const MotionTag = motion[as] as typeof motion.div;
const item: Variants = {
hidden: reduce ? { opacity: 1 } : { opacity: 0, y, filter: "blur(4px)" },
show: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.75, ease: EASE_OUT },
},
};
return (
<MotionTag className={className} variants={item}>
{children}
</MotionTag>
);
}