agency-web/app/components/ServicesJourney.tsx

163 lines
5.2 KiB
TypeScript

"use client";
/**
* ServicesJourney — pinned horizontal-scroll track of services.
*
* On desktop the section pins and the panels scroll sideways as you scroll
* down (GSAP ScrollTrigger pin + scrub), with a progress bar. On mobile / when
* the pin would be awkward, and under reduced-motion, it degrades to a normal
* vertical stack (no pin, panels just stack and reveal). Each panel is a real
* <article> so the content is accessible regardless of layout.
*/
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const services = [
{
n: "01",
t: "Web & Desarrollo",
d: "Sitios construidos en código, veloces y 100% editables. Sin plantillas, sin atajos: arquitectura que escala contigo.",
tags: ["Next.js", "Headless", "Core Web Vitals"],
},
{
n: "02",
t: "SEO & GEO",
d: "Posicionamiento técnico y de contenido — para Google y para los motores de IA. Reporting real, no humo.",
tags: ["Técnico", "Contenido", "Answer Engines"],
},
{
n: "03",
t: "Paid Ads",
d: "Google, Meta, TikTok y LinkedIn. Creatividad asistida por IA y pujas optimizadas hacia el resultado, no el clic.",
tags: ["Performance", "Creatividad IA", "Atribución"],
},
{
n: "04",
t: "Contenido",
d: "Piezas pensadas para rankear y para convertir. Producción a escala sin perder la voz de tu marca.",
tags: ["Editorial", "Vídeo", "Social"],
},
{
n: "05",
t: "Diseño & Marca",
d: "Identidad y dirección de arte con criterio. Sistemas de diseño que viven en producto, no en un PDF.",
tags: ["Identidad", "Design System", "Motion"],
},
{
n: "06",
t: "Automatización",
d: "Flujos que devuelven horas: captación de leads, reporting automático y operativa conectada de punta a punta.",
tags: ["Leads", "Reporting", "Ops"],
},
];
export default function ServicesJourney() {
const section = useRef<HTMLElement>(null);
const track = useRef<HTMLDivElement>(null);
const bar = useRef<HTMLSpanElement>(null);
useEffect(() => {
const sec = section.current;
const trk = track.current;
if (!sec || !trk) return;
const mm = gsap.matchMedia();
// Desktop with motion allowed: pin + horizontal scroll.
mm.add(
"(min-width: 900px) and (prefers-reduced-motion: no-preference)",
() => {
const distance = () => trk.scrollWidth - window.innerWidth;
const tween = gsap.to(trk, {
x: () => -distance(),
ease: "none",
scrollTrigger: {
trigger: sec,
start: "top top",
end: () => "+=" + distance(),
scrub: 0.6,
pin: true,
invalidateOnRefresh: true,
onUpdate: (self) => {
if (bar.current) {
bar.current.style.transform = `scaleX(${self.progress})`;
}
},
},
});
// Per-panel inner reveal as it slides into center.
gsap.utils.toArray<HTMLElement>(".sjourney__panel").forEach((panel) => {
gsap.from(panel.querySelectorAll(".sjourney__reveal"), {
y: 40,
opacity: 0,
duration: 0.6,
stagger: 0.06,
ease: "power3.out",
scrollTrigger: {
trigger: panel,
containerAnimation: tween,
start: "left 80%",
},
});
});
return () => {
if (bar.current) bar.current.style.transform = "scaleX(0)";
};
}
);
// Mobile / reduced-motion: simple stacked reveals.
mm.add("(max-width: 899px)", () => {
gsap.utils.toArray<HTMLElement>(".sjourney__panel").forEach((panel) => {
gsap.from(panel, {
y: 36,
opacity: 0,
duration: 0.7,
ease: "power3.out",
scrollTrigger: { trigger: panel, start: "top 88%" },
});
});
});
return () => mm.revert();
}, []);
return (
<section id="servicios" className="sjourney" ref={section} aria-label="Servicios">
<div className="sjourney__head wrap">
<p className="kicker">02 Qué hacemos</p>
<h2 className="sjourney__title">
Todo tu crecimiento, <span className="serif-em">un mismo sistema.</span>
</h2>
<div className="sjourney__progress" aria-hidden="true">
<span ref={bar} className="sjourney__progress-bar" />
</div>
</div>
<div className="sjourney__viewport">
<div className="sjourney__track" ref={track}>
{services.map((s) => (
<article className="sjourney__panel hoverable" key={s.t} data-cursor="explorar">
<span className="sjourney__n sjourney__reveal">{s.n}</span>
<h3 className="sjourney__panel-title sjourney__reveal">{s.t}</h3>
<p className="sjourney__panel-desc sjourney__reveal">{s.d}</p>
<ul className="sjourney__tags sjourney__reveal">
{s.tags.map((tag) => (
<li key={tag}>{tag}</li>
))}
</ul>
<span className="sjourney__pill" aria-hidden="true" />
</article>
))}
</div>
</div>
</section>
);
}