SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
1.7 KB · 44 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { cn } from "@/lib/utils";45/**6 * Subtle fade-up on scroll. Content is rendered visible on the server (SEO, no-JS, screenshots);7 * once mounted, elements still below the fold are hidden and revealed when they enter the viewport.8 * Respects prefers-reduced-motion via the global CSS rule (transitions collapse to ~0 ms).9 */10export function Reveal({ children, className, delay = 0, as = "div" }: { children: React.ReactNode; className?: string; delay?: number; as?: "div" | "section" | "li" }) {11  const ref = React.useRef<HTMLElement | null>(null);12  const [state, setState] = React.useState<"ssr" | "hidden" | "shown">("ssr");1314  React.useEffect(() => {15    const el = ref.current;16    if (!el || typeof IntersectionObserver === "undefined") return;17    const rect = el.getBoundingClientRect();18    if (rect.top < window.innerHeight * 0.95) return; // already on screen: never hide19    setState("hidden");20    const io = new IntersectionObserver(21      (entries) => {22        if (entries.some((e) => e.isIntersecting)) {23          setState("shown");24          io.disconnect();25        }26      },27      { rootMargin: "0px 0px -60px 0px", threshold: 0.05 },28    );29    io.observe(el);30    return () => io.disconnect();31  }, []);3233  const Tag = as as "div";34  return (35    <Tag36      ref={ref as React.RefObject<HTMLDivElement>}37      className={cn(className, "transition-[opacity,transform] duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] will-change-[opacity,transform]", state === "hidden" && "opacity-0 translate-y-3")}38      style={state !== "ssr" ? { transitionDelay: `${delay}s` } : undefined}39    >40      {children}41    </Tag>42  );43}44