'use client'; import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import './ScrollExpand.css'; const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); const smoothstep = (edge0, edge1, x) => { const t = clamp((x - edge0) / (edge1 - edge0 || 1e-6), 0, 1); return t * t * (3 - 2 * t); }; const ScrollExpand = ({ src = '', mediaType = 'image', poster = '', alt = '', title = '', scrollHint = '', startWidth = 42, startHeight = 58, startRadius = 24, endRadius = 0, mediaZoom = 1.35, scrollDistance = 1.2, holdDistance = 0.35, smoothing = 0.1, overlayScrim = 0.45, useWindowScroll = false, enabled = true, children, className = '', style, ...rest }) => { const rootRef = useRef(null); const trackRef = useRef(null); const stageRef = useRef(null); const frameRef = useRef(null); const mediaRef = useRef(null); const titleRef = useRef(null); const overlayRef = useRef(null); const scrimRef = useRef(null); const hintRef = useRef(null); const propsRef = useRef({}); useLayoutEffect(() => { propsRef.current = { startWidth, startHeight, startRadius, endRadius, mediaZoom, scrollDistance, holdDistance, smoothing, overlayScrim, useWindowScroll, enabled }; }); const applyProgress = useCallback(p => { const frame = frameRef.current; const media = mediaRef.current; if (!frame || !media) return; const c = propsRef.current; const e = smoothstep(0, 1, p); const w = c.startWidth + (100 - c.startWidth) * e; const h = c.startHeight + (100 - c.startHeight) * e; const ix = Math.max(0, (100 - w) / 2); const iy = Math.max(0, (100 - h) / 2); const r = c.startRadius + (c.endRadius - c.startRadius) * e; frame.style.clipPath = `inset(${iy}% ${ix}% ${iy}% ${ix}% round ${r}px)`; media.style.transform = `scale(${c.mediaZoom + (1 - c.mediaZoom) * e})`; if (scrimRef.current) scrimRef.current.style.opacity = `${c.overlayScrim * e}`; if (titleRef.current) { const out = smoothstep(0.4, 0.88, p); titleRef.current.style.opacity = `${1 - out}`; titleRef.current.style.transform = `translate3d(0, ${-28 * out}px, 0) scale(${1 + 0.06 * out})`; } if (hintRef.current) { const gone = smoothstep(0, 0.12, p); hintRef.current.style.opacity = `${1 - gone}`; hintRef.current.style.transform = `translate3d(0, ${8 * gone}px, 0)`; } if (overlayRef.current) { const inn = smoothstep(0.68, 1, p); overlayRef.current.style.opacity = `${inn}`; overlayRef.current.style.transform = `translate3d(0, ${18 * (1 - inn)}px, 0)`; } }, []); useEffect(() => { const root = rootRef.current; const track = trackRef.current; const stage = stageRef.current; if (!root || !track || !stage) return; const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; let raf = 0; let current = 0; let target = 0; let stageH = 0; let running = false; const measure = () => { const c = propsRef.current; stageH = c.useWindowScroll ? window.innerHeight : root.clientHeight; if (stageH <= 0) return; stage.style.height = `${stageH}px`; track.style.height = `${stageH * (1 + Math.max(0, c.scrollDistance) + Math.max(0, c.holdDistance))}px`; const w = root.clientWidth || stageH; stage.style.setProperty('--se-title-size', `${clamp(w * 0.075, 20, 84)}px`); }; const readProgress = () => { const c = propsRef.current; if (!c.enabled) return 1; const span = stageH * Math.max(0.01, c.scrollDistance); if (c.useWindowScroll) { const top = track.getBoundingClientRect().top; return clamp(-top / span, 0, 1); } return clamp(root.scrollTop / span, 0, 1); }; const tick = () => { const c = propsRef.current; const k = c.smoothing <= 0 ? 1 : 1 - Math.exp(-1 / (60 * c.smoothing)); current += (target - current) * k; if (Math.abs(target - current) < 0.0004) { current = target; running = false; } applyProgress(current); raf = running ? requestAnimationFrame(tick) : 0; }; const kick = () => { if (running) return; running = true; if (!raf) raf = requestAnimationFrame(tick); }; const onScroll = () => { target = readProgress(); if (propsRef.current.smoothing <= 0 || reduceMotion) { current = target; applyProgress(current); return; } kick(); }; const onResize = () => { measure(); target = readProgress(); current = target; applyProgress(current); }; measure(); target = readProgress(); current = target; applyProgress(current); const scroller = useWindowScroll ? window : root; scroller.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('resize', onResize); const ro = new ResizeObserver(onResize); ro.observe(root); return () => { if (raf) cancelAnimationFrame(raf); scroller.removeEventListener('scroll', onScroll); window.removeEventListener('resize', onResize); ro.disconnect(); }; }, [applyProgress, useWindowScroll]); const media = mediaType === 'video' ? (