From 4223004c24b7900b408475385aa83a7b61f0e613 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 13 Jun 2026 00:06:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(deepfield):=20ScrollReveal=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=97=B0=EC=B6=9C=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/deepfield/ScrollReveal.tsx | 53 +++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 app/components/deepfield/ScrollReveal.tsx diff --git a/app/components/deepfield/ScrollReveal.tsx b/app/components/deepfield/ScrollReveal.tsx new file mode 100644 index 0000000..14c7fd1 --- /dev/null +++ b/app/components/deepfield/ScrollReveal.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +interface Props { + children: React.ReactNode; + /** 등장 지연(ms) — 연속 항목 스태거용 */ + delay?: number; + /** 'fade-up'(기본) | 'fade' | 'draw'(선 그리기용 — width 확장) */ + variant?: 'fade-up' | 'fade' | 'draw'; + className?: string; +} + +export default function ScrollReveal({ children, delay = 0, variant = 'fade-up', className }: Props) { + const ref = useRef(null); + const [shown, setShown] = useState(false); + + useEffect(() => { + // reduced-motion: 즉시 표시 (연출 생략) + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setShown(true); + return; + } + const el = ref.current; + if (!el) return; + const io = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + setShown(true); + io.disconnect(); + } + }, + { threshold: 0.2 }, + ); + io.observe(el); + return () => io.disconnect(); + }, []); + + const hidden = + variant === 'fade' ? 'opacity-0' : + variant === 'draw' ? 'opacity-0 [transform:scaleX(0)] origin-left' : + 'opacity-0 translate-y-6'; + + return ( +
+ {children} +
+ ); +}