import React, { useEffect, useRef } from "react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; gsap.registerPlugin(ScrollTrigger); type RevealParagraphProps = { children: React.ReactNode; className?: string; // cuánto de la pantalla para start/end del reveal start?: string; // ej: "top 90%" end?: string; // ej: "bottom 20%" }; export function RevealParagraph({ children, className = "", start = "top 100%", end = "top 30%", }: RevealParagraphProps) { const rootRef = useRef(null); // Extraer el texto y dividirlo en líneas lógicas const getTextContent = (node: React.ReactNode): string => { if (typeof node === "string") return node; if (typeof node === "number") return String(node); if (React.isValidElement(node) && node.props.children) { return React.Children.toArray(node.props.children) .map(getTextContent) .join(""); } return ""; }; const textContent = getTextContent(children); // Dividir el texto en líneas: primero por saltos de línea explícitos, // luego si es muy largo, dividir por oraciones (puntos seguidos de espacio) let lines: string[] = []; if (textContent.includes("\n")) { lines = textContent.split(/\n/).filter((line) => line.trim() !== ""); } else { // Dividir por oraciones (puntos, signos de interrogación, exclamación) const sentences = textContent.split(/(?<=[.!?])\s+/).filter((s) => s.trim() !== ""); if (sentences.length > 1) { lines = sentences; } else { // Si es una sola oración muy larga, dividir por comas if (textContent.length > 150) { const commaSplit = textContent.split(/(?<=,)\s+/).filter((s) => s.trim() !== ""); lines = commaSplit.length > 1 ? commaSplit : [textContent]; } else { lines = [textContent]; } } } useEffect(() => { const ctx = gsap.context(() => { if (!rootRef.current) return; // Obtener todas las líneas const lineElements = rootRef.current.querySelectorAll(".reveal-line"); if (lineElements.length === 0) return; // Obtener todos los reveal spans const revealSpans = Array.from(lineElements).map((line) => line.querySelector(".reveal-span") ).filter(Boolean) as HTMLElement[]; if (revealSpans.length === 0) return; // Usar un solo ScrollTrigger para todas las líneas const staggerAmount = 0.25; // 25% de overlap entre líneas ScrollTrigger.create({ trigger: rootRef.current, start, end, scrub: true, onUpdate: (self) => { const globalProgress = self.progress; revealSpans.forEach((span, index) => { // Calcular el rango de progress para esta línea const lineStart = index * staggerAmount; const lineDuration = 1 - (revealSpans.length - 1) * staggerAmount; const lineEnd = lineStart + lineDuration; // Mapear el progress global al progress de esta línea let lineProgress = 0; if (globalProgress >= lineStart && globalProgress <= lineEnd) { lineProgress = (globalProgress - lineStart) / lineDuration; } else if (globalProgress > lineEnd) { lineProgress = 1; } lineProgress = Math.max(0, Math.min(1, lineProgress)); span.style.setProperty("--clipRight", `${(1 - lineProgress) * 100}%`); }); }, }); }, rootRef); return () => ctx.revert(); }, [start, end, lines.length]); return (
{lines.map((line, index) => (
{/* Capa base: apagado */} {line.trim()} {/* Capa reveal: blanco puro, recortado */} {line.trim()}
))}
); }