"use client";
import * as React from "react";
import { motion, useInView, useReducedMotion, type TargetAndTransition, type UseInViewOptions } from "motion/react";
import { cn } from "@/lib/utils";
export type ScrollRevealPreset = "fade-up" | "fade-down" | "zoom" | "slide-left" | "slide-right" | "blur" | "flip";
export interface ScrollRevealProps {
children: React.ReactNode;
preset?: ScrollRevealPreset;
/** Seconds before the animation starts once in view. */
delay?: number;
/** Seconds the animation takes. */
duration?: number;
/** Travel distance in px for fade / slide presets. */
distance?: number;
/** When `children` is a list of elements, reveal them one after another with this gap (s). */
stagger?: number;
/** Only animate the first time. */
once?: boolean;
/** Portion (0–1) of the element that must be visible. */
amount?: number;
/** Scroll container to observe against (defaults to the viewport). */
root?: React.RefObject<Element | null>;
/** Grow the viewport margin, e.g. "0px 0px -10% 0px" to trigger a bit later. */
margin?: UseInViewOptions["margin"];
as?: "div" | "section" | "li" | "ul" | "article";
className?: string;
/** Class for each wrapper when `stagger` is used. */
itemClassName?: string;
}
function hiddenFor(preset: ScrollRevealPreset, d: number): TargetAndTransition {
switch (preset) {
case "fade-up":
return { opacity: 0, y: d };
case "fade-down":
return { opacity: 0, y: -d };
case "zoom":
return { opacity: 0, scale: 0.88 };
case "slide-left":
return { opacity: 0, x: d * 1.5 };
case "slide-right":
return { opacity: 0, x: -d * 1.5 };
case "blur":
return { opacity: 0, filter: "blur(14px)", scale: 1.04 };
case "flip":
return { opacity: 0, rotateX: -35, y: d * 0.6, transformPerspective: 800 };
}
}
const shown: TargetAndTransition = { opacity: 1, x: 0, y: 0, scale: 1, rotateX: 0, filter: "blur(0px)" };
/** Animates its children in when they scroll into view. */
export function ScrollReveal({
children,
preset = "fade-up",
delay = 0,
duration = 0.7,
distance = 32,
stagger,
once = true,
amount = 0.3,
root,
margin,
as = "div",
className,
itemClassName,
}: ScrollRevealProps) {
const reduce = useReducedMotion();
const ref = React.useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once, amount, root, margin });
const hidden = reduce ? { opacity: 0 } : hiddenFor(preset, distance);
const transition = (i: number) => ({
delay: delay + i * (stagger ?? 0),
duration: reduce ? 0.3 : duration,
ease: [0.22, 1, 0.36, 1] as const,
});
const Comp = motion[as] as typeof motion.div;
if (stagger !== undefined) {
const items = React.Children.toArray(children);
return (
<Comp ref={ref} className={className}>
{items.map((child, i) => (
<motion.div
key={i}
initial={hidden}
animate={inView ? shown : hidden}
transition={transition(i)}
className={cn(itemClassName)}
>
{child}
</motion.div>
))}
</Comp>
);
}
return (
<Comp ref={ref} initial={hidden} animate={inView ? shown : hidden} transition={transition(0)} className={className}>
{children}
</Comp>
);
}