feat(grafico): HiloConductor — la línea dorada que cose la página

This commit is contained in:
erikgh committed 2026-07-31 11:09:59 -06:00
1 parent 6783e32324
commit a900bdd99d
3 files changed
+105 -1

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
'use client';
import { motion, useScroll, useSpring } from 'framer-motion';
import { usePrefersReducedMotion } from '@/lib/motion';
// One continuous gold line running the height of the page, drawing itself as
// the reader scrolls. It is the spine of the layout: it stitches the sections
// together so the landing reads as a route rather than a stack of blocks.
//
// Positioned `absolute`, never `fixed` — the thread belongs to the document
// and scrolls with it. A fixed layer would read as an ornament stuck to the
// window.
//
// Uses the page-level useScroll() with no target ref on purpose: no element
// measuring, no "container has a static position" warnings, and one scroll
// listener for the whole page.
const TRAZO =
'M50 0 C 20 140, 80 260, 50 400 C 20 540, 80 660, 50 800 C 20 940, 80 1060, 50 1200';
export function HiloConductor({ className = '' }: { className?: string }) {
const reduced = usePrefersReducedMotion();
const { scrollYProgress } = useScroll();
const avance = useSpring(scrollYProgress, { stiffness: 90, damping: 30, restDelta: 0.001 });
const base = `pointer-events-none absolute inset-y-0 left-1/2 -translate-x-1/2 w-full max-w-content h-full ${className}`;
// The positioning/pointer-events classes live on a wrapping <div> rather
// than the <svg> itself: per spec `SVGElement.className` is an
// SVGAnimatedString, not a plain string, so tests (and any other reader of
// `.className`) can't inspect it as text. A plain HTMLElement keeps that
// contract simple.
// Plain <path> for reduced motion: motion.path applies dash scaffolding even
// without initial/animate and would leave the thread invisible.
if (reduced) {
return (
<div className={base}>
<svg
className="h-full w-full text-dorado/25"
viewBox="0 0 100 1200"
preserveAspectRatio="none"
aria-hidden="true"
focusable="false"
>
<path d={TRAZO} fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" />
</svg>
</div>
);
}
return (
<div className={base}>
<svg
className="h-full w-full text-dorado/25"
viewBox="0 0 100 1200"
preserveAspectRatio="none"
aria-hidden="true"
focusable="false"
>
<motion.path
d={TRAZO}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
style={{ pathLength: avance }}
/>
</svg>
</div>
);
}