Motion y hooks
Animaciones sin dependencias que respetan reduced-motion, y los hooks base del ecosistema.
import { AnimatedSection, AnimatedCounter, BackToTop } from "@fesa/components/motion";
import { useInView, useIsMobile, useHydrated } from "@fesa/components/hooks";Todo CSS puro + requestAnimationFrame — cero librerías de animación en el bundle — y respetando prefers-reduced-motion.
AnimatedSection
Revela su contenido al entrar en viewport.
<AnimatedSection variant="fade-up" delay={100}>
<FeatureCard />
</AnimatedSection>| Prop | Tipo | Default | Descripción |
|---|---|---|---|
variant | 'fade' | 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'scale' | 'slide-up' | 'fade-up' | Tipo de entrada |
delay | number | 0 | Retardo en ms (para escalonar listas: delay={index * 100}) |
threshold | number | — | Cuánto del elemento debe verse para disparar |
as | React.ElementType | 'div' | Elemento contenedor |
StaggerChildren — variante que anima a los hijos en cascada con staggerDelay.
AnimatedCounter
Contador animado al entrar en viewport; parsea prefijo/sufijo/decimales del string ("+1.500 clientes", "99,9%").
<AnimatedCounter value="+1.500" className="text-3xl font-bold" />BackToTop
Botón flotante de volver arriba, con throttle por rAF (aparece tras scrollear). Etiqueta accesible por prop.
Hooks
useInView
IntersectionObserver con ref-callback de React 19 (cleanup automático).
const { ref, isInView, hasBeenInView } = useInView({ triggerOnce: true, threshold: 0.2 });Antes existía en cuatro copias byte-idénticas — es el símbolo de por qué existe esta librería.
useIsMobile
Breakpoint (< 768px) vía useSyncExternalStore sobre matchMedia: sin flash de SSR, sin useEffect, sin re-render extra.
useHydrated
true tras la hidratación — para gates de contenido client-only sin parpadeo.
useReducedMotionStore
La preferencia prefers-reduced-motion como store de zustand, compartida entre componentes.