Fazekit

Code

"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>
  );
}

More in Motion & Scroll

View all →