Files
cfw-autumn/apps/website/components/feature-icon-animation.jsx
2026-04-15 16:39:41 +01:00

69 lines
1.9 KiB
JavaScript
Executable File

"use client";
import { forwardRef, useImperativeHandle, useRef, useEffect } from "react";
import gsap from "gsap";
export const FeatureIconAnimation = forwardRef(({ Icon }, ref) => {
const iconRef = useRef(null);
const tlRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => tlRef.current?.play(),
reverse: () => tlRef.current?.reverse(),
}));
useEffect(() => {
const pixelEls = iconRef.current?.querySelectorAll("path");
if (!pixelEls?.length) return;
// Sort pixels by distance from center — center pixels animate first, outer ones last
const centers = Array.from(pixelEls).map((el) => {
const b = el.getBBox();
return { el, cx: b.x + b.width / 2, cy: b.y + b.height / 2 };
});
const midX = centers.reduce((s, c) => s + c.cx, 0) / centers.length;
const midY = centers.reduce((s, c) => s + c.cy, 0) / centers.length;
const pixels = centers
.sort((a, b) => {
const da = (a.cx - midX) ** 2 + (a.cy - midY) ** 2;
const db = (b.cx - midX) ** 2 + (b.cy - midY) ** 2;
return da - db;
})
.map((c) => c.el);
gsap.set(pixels, {
opacity: 0.15,
scale: 0.8,
transformOrigin: "center center",
fill: "currentColor",
});
tlRef.current = gsap.timeline({ paused: true });
tlRef.current
.to(pixels, {
opacity: 1,
scale: 1.15,
fill: "#9564ff",
duration: 0.2,
stagger: 0.04,
ease: "power2.out",
})
.to(pixels, {
scale: 1,
duration: 0.15,
ease: "back.out(3)",
});
return () => tlRef.current?.kill();
}, []);
return (
<div className="relative flex items-center justify-start w-12 h-12 group/icon">
<div className="relative w-[24px] h-[24px] flex items-center justify-center">
<Icon ref={iconRef} className="w-full h-full text-white" />
</div>
</div>
);
});
FeatureIconAnimation.displayName = "FeatureIconAnimation";