← All primitives
VelocitySkew
MotionThe 'liquid scroll' feel — skews its children by scroll velocity (read from Lenis), clamped and eased back to flat. No second RAF loop.
Reads Lenis velocity via useLenis; needs a ReactLenis root.
$
npx shadcn@latest add https://prism.icglabs.co/r/velocity-skew.jsonDependencies:gsaplenis
View raw manifest →components/prism/VelocitySkew.tsx
"use client";
import { useEffect, useRef } from "react";
import { useLenis } from "lenis/react";
import gsap from "gsap";
/**
* VelocitySkew — the "liquid scroll" feel: skews its children by scroll velocity,
* clamped and eased back to flat. Reads Lenis velocity from the existing root
* instance (no second RAF loop) and drives a gsap.quickTo for smoothing. Honors
* prefers-reduced-motion.
*/
export function VelocitySkew({
children,
className = "",
strength = 0.5,
max = 8,
}: {
children: React.ReactNode;
className?: string;
/** velocity → degrees multiplier */
strength?: number;
/** clamp in degrees */
max?: number;
}) {
const ref = useRef<HTMLDivElement>(null);
const skewTo = useRef<((v: number) => void) | null>(null);
const reduced = useRef(false);
useEffect(() => {
reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const el = ref.current;
if (!el || reduced.current) return;
skewTo.current = gsap.quickTo(el, "skewY", { duration: 0.5, ease: "power3.out" });
return () => {
gsap.killTweensOf(el);
skewTo.current = null;
};
}, []);
useLenis((lenis: { velocity: number }) => {
if (reduced.current || !skewTo.current) return;
const v = Math.max(-max, Math.min(max, lenis.velocity * strength));
skewTo.current(v);
});
return (
<div ref={ref} className={className} style={{ willChange: "transform" }}>
{children}
</div>
);
}