fix how to work , about,blog
This commit is contained in:
103
src/animations/AnimationProvider.tsx
Normal file
103
src/animations/AnimationProvider.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
/**
|
||||
* AnimationProvider
|
||||
* Initializes GSAP + ScrollTrigger globally, refreshes on route changes,
|
||||
* and provides smooth defaults.
|
||||
*/
|
||||
export default function AnimationProvider({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// Global GSAP defaults for buttery animations
|
||||
gsap.defaults({
|
||||
ease: "power3.out",
|
||||
duration: 0.8,
|
||||
});
|
||||
|
||||
const initDecorativeBlocks = () => {
|
||||
// Clean up previous block triggers to avoid duplicates
|
||||
ScrollTrigger.getAll().forEach((t) => {
|
||||
if (t.vars && (t.vars as any).id === "block-deco") {
|
||||
t.kill();
|
||||
}
|
||||
});
|
||||
|
||||
const decorativeBlocks = document.querySelectorAll(".block-decoration");
|
||||
decorativeBlocks.forEach((block) => {
|
||||
ScrollTrigger.create({
|
||||
id: "block-deco",
|
||||
trigger: block,
|
||||
start: "top 92%",
|
||||
onEnter: () => {
|
||||
setTimeout(() => {
|
||||
block.classList.add("animated");
|
||||
}, 150);
|
||||
},
|
||||
onEnterBack: () => {
|
||||
setTimeout(() => {
|
||||
block.classList.add("animated");
|
||||
}, 150);
|
||||
},
|
||||
onLeave: () => {
|
||||
block.classList.remove("animated");
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
block.classList.remove("animated");
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Run initializations
|
||||
initDecorativeBlocks();
|
||||
|
||||
// Refresh ScrollTrigger at staggered intervals to account for lazy-loaded assets/images over the network
|
||||
const timeouts = [
|
||||
setTimeout(() => {
|
||||
initDecorativeBlocks();
|
||||
ScrollTrigger.refresh(true);
|
||||
}, 100),
|
||||
setTimeout(() => {
|
||||
initDecorativeBlocks();
|
||||
ScrollTrigger.refresh(true);
|
||||
}, 500),
|
||||
setTimeout(() => {
|
||||
initDecorativeBlocks();
|
||||
ScrollTrigger.refresh(true);
|
||||
}, 1500),
|
||||
setTimeout(() => {
|
||||
initDecorativeBlocks();
|
||||
ScrollTrigger.refresh(true);
|
||||
}, 3000),
|
||||
];
|
||||
|
||||
// Refresh on full window load (when all images, styles, and fonts are in place)
|
||||
const handleLoad = () => {
|
||||
initDecorativeBlocks();
|
||||
ScrollTrigger.refresh(true);
|
||||
};
|
||||
window.addEventListener("load", handleLoad);
|
||||
|
||||
// Also refresh on window resize
|
||||
const handleResize = () => {
|
||||
initDecorativeBlocks();
|
||||
ScrollTrigger.refresh(true);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
timeouts.forEach(clearTimeout);
|
||||
window.removeEventListener("load", handleLoad);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
ScrollTrigger.getAll().forEach((t) => t.kill());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
680
src/animations/Reveal.tsx
Normal file
680
src/animations/Reveal.tsx
Normal file
@@ -0,0 +1,680 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import gsap from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
// Register ScrollTrigger safely
|
||||
if (typeof window !== "undefined") {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
1. ScrollReveal
|
||||
Fade-in + slide-up on scroll. The workhorse animation.
|
||||
============================================================ */
|
||||
interface ScrollRevealProps {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
yOffset?: number;
|
||||
xOffset?: number;
|
||||
className?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function ScrollReveal({
|
||||
children,
|
||||
delay = 0,
|
||||
duration = 0.8,
|
||||
yOffset = 40,
|
||||
xOffset = 0,
|
||||
className = "",
|
||||
triggerOnce = false,
|
||||
}: ScrollRevealProps) {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elementRef.current;
|
||||
if (!el) return;
|
||||
|
||||
gsap.set(el, { y: yOffset, x: xOffset, opacity: 0 });
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 88%",
|
||||
onEnter: (self) => {
|
||||
gsap.to(el, {
|
||||
y: 0,
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
if (triggerOnce) self.kill();
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.to(el, {
|
||||
y: 0,
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
},
|
||||
onLeave: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(el, { y: yOffset, x: xOffset, opacity: 0 });
|
||||
}
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(el, { y: yOffset, x: xOffset, opacity: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => trigger?.kill();
|
||||
}, [delay, duration, yOffset, xOffset, triggerOnce]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
2. RevealText
|
||||
Splits text into words or characters and reveals with slide-up mask.
|
||||
============================================================ */
|
||||
interface RevealTextProps {
|
||||
children: string;
|
||||
type?: "words" | "chars";
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function RevealText({
|
||||
children,
|
||||
type = "words",
|
||||
delay = 0,
|
||||
duration = 0.85,
|
||||
className = "",
|
||||
triggerOnce = false,
|
||||
}: RevealTextProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const items = el.querySelectorAll(".reveal-item");
|
||||
if (!items.length) return;
|
||||
|
||||
gsap.set(items, { y: "110%", opacity: 0 });
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 88%",
|
||||
onEnter: (self) => {
|
||||
gsap.to(items, {
|
||||
y: "0%",
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power4.out",
|
||||
stagger: type === "chars" ? 0.02 : 0.04,
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
if (triggerOnce) self.kill();
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.to(items, {
|
||||
y: "0%",
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power4.out",
|
||||
stagger: type === "chars" ? 0.02 : 0.04,
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
},
|
||||
onLeave: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(items, { y: "110%", opacity: 0 });
|
||||
}
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(items, { y: "110%", opacity: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => trigger?.kill();
|
||||
}, [children, type, delay, duration, triggerOnce]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (type === "chars") {
|
||||
return children.split("").map((char, i) => (
|
||||
<span key={i} className="inline-block overflow-hidden" style={{ height: "1.2em", lineHeight: "1.2em", verticalAlign: "middle" }}>
|
||||
<span className="reveal-item inline-block">{char === " " ? "\u00A0" : char}</span>
|
||||
</span>
|
||||
));
|
||||
}
|
||||
return children.split(" ").map((word, i) => (
|
||||
<span key={i} className="inline-block overflow-hidden" style={{ height: "1.2em", lineHeight: "1.2em", marginRight: "0.25em", verticalAlign: "middle" }}>
|
||||
<span className="reveal-item inline-block">{word}</span>
|
||||
</span>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={className} style={{ display: "flex", flexWrap: "wrap" }}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
3. Magnetic
|
||||
Cursor-tracking magnetic pull on elements.
|
||||
============================================================ */
|
||||
interface MagneticProps {
|
||||
children: React.ReactElement;
|
||||
range?: number;
|
||||
strength?: number;
|
||||
}
|
||||
|
||||
export function Magnetic({ children, range = 45, strength = 0.35 }: MagneticProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const child = el.firstElementChild as HTMLElement;
|
||||
if (!child) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const relX = e.clientX - (rect.left + rect.width / 2);
|
||||
const relY = e.clientY - (rect.top + rect.height / 2);
|
||||
const dist = Math.sqrt(relX * relX + relY * relY);
|
||||
|
||||
if (dist < range) {
|
||||
gsap.to(child, { x: relX * strength, y: relY * strength, ease: "power2.out", duration: 0.4 });
|
||||
} else {
|
||||
gsap.to(child, { x: 0, y: 0, ease: "elastic.out(1.2, 0.4)", duration: 0.8 });
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
gsap.to(child, { x: 0, y: 0, ease: "elastic.out(1.2, 0.4)", duration: 0.8 });
|
||||
};
|
||||
|
||||
el.addEventListener("mousemove", handleMouseMove);
|
||||
el.addEventListener("mouseleave", handleMouseLeave);
|
||||
return () => {
|
||||
el.removeEventListener("mousemove", handleMouseMove);
|
||||
el.removeEventListener("mouseleave", handleMouseLeave);
|
||||
};
|
||||
}, [range, strength]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="inline-block">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
4. ShimmerText
|
||||
Premium shimmering gradient sweep on text.
|
||||
============================================================ */
|
||||
interface ShimmerTextProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ShimmerText({ children, className = "" }: ShimmerTextProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-block bg-clip-text text-transparent bg-[linear-gradient(110deg,#ffffff,45%,#c01227,55%,#ffffff)] bg-[length:250%_100%] animate-[shimmer-sweep_6s_infinite_linear] ${className}`}
|
||||
style={{ WebkitBackgroundClip: "text", backgroundClip: "text" }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
5. StaggerChildren
|
||||
Wraps children and reveals them with staggered scroll animation.
|
||||
============================================================ */
|
||||
interface StaggerChildrenProps {
|
||||
children: React.ReactNode;
|
||||
stagger?: number;
|
||||
duration?: number;
|
||||
yOffset?: number;
|
||||
className?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function StaggerChildren({
|
||||
children,
|
||||
stagger = 0.1,
|
||||
duration = 0.7,
|
||||
yOffset = 35,
|
||||
className = "",
|
||||
triggerOnce = false,
|
||||
}: StaggerChildrenProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const items = el.children;
|
||||
if (!items.length) return;
|
||||
|
||||
gsap.set(items, { y: yOffset, opacity: 0 });
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 85%",
|
||||
onEnter: (self) => {
|
||||
gsap.to(items, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
stagger,
|
||||
overwrite: "auto",
|
||||
});
|
||||
if (triggerOnce) self.kill();
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.to(items, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
stagger,
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
},
|
||||
onLeave: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(items, { y: yOffset, opacity: 0 });
|
||||
}
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(items, { y: yOffset, opacity: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => trigger?.kill();
|
||||
}, [stagger, duration, yOffset, triggerOnce]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
6. ScaleReveal
|
||||
Scales from 0.85 → 1.0 + fades in on scroll. Great for cards/images.
|
||||
============================================================ */
|
||||
interface ScaleRevealProps {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function ScaleReveal({
|
||||
children,
|
||||
delay = 0,
|
||||
duration = 0.8,
|
||||
className = "",
|
||||
triggerOnce = false,
|
||||
}: ScaleRevealProps) {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elementRef.current;
|
||||
if (!el) return;
|
||||
|
||||
gsap.set(el, { scale: 0.85, opacity: 0 });
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 88%",
|
||||
onEnter: (self) => {
|
||||
gsap.to(el, {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
if (triggerOnce) self.kill();
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.to(el, {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
},
|
||||
onLeave: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(el, { scale: 0.85, opacity: 0 });
|
||||
}
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(el, { scale: 0.85, opacity: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => trigger?.kill();
|
||||
}, [delay, duration, triggerOnce]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
7. SlideReveal
|
||||
Slides from left or right with smooth reveal.
|
||||
============================================================ */
|
||||
interface SlideRevealProps {
|
||||
children: React.ReactNode;
|
||||
direction?: "left" | "right";
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function SlideReveal({
|
||||
children,
|
||||
direction = "left",
|
||||
delay = 0,
|
||||
duration = 0.9,
|
||||
className = "",
|
||||
triggerOnce = false,
|
||||
}: SlideRevealProps) {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elementRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const xStart = direction === "left" ? -60 : 60;
|
||||
gsap.set(el, { x: xStart, opacity: 0 });
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 88%",
|
||||
onEnter: (self) => {
|
||||
gsap.to(el, {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
if (triggerOnce) self.kill();
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.to(el, {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
duration,
|
||||
ease: "power3.out",
|
||||
delay,
|
||||
overwrite: "auto",
|
||||
});
|
||||
}
|
||||
},
|
||||
onLeave: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(el, { x: xStart, opacity: 0 });
|
||||
}
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
if (!triggerOnce) {
|
||||
gsap.set(el, { x: xStart, opacity: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => trigger?.kill();
|
||||
}, [direction, delay, duration, triggerOnce]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
8. ParallaxSection
|
||||
Subtle parallax depth on scroll for images/backgrounds.
|
||||
============================================================ */
|
||||
interface ParallaxSectionProps {
|
||||
children: React.ReactNode;
|
||||
speed?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ParallaxSection({
|
||||
children,
|
||||
speed = 0.15,
|
||||
className = "",
|
||||
}: ParallaxSectionProps) {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elementRef.current;
|
||||
if (!el) return;
|
||||
|
||||
// Check for reduced motion preference
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
gsap.to(el, {
|
||||
y: () => -ScrollTrigger.maxScroll(window) * speed * 0.1,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "top bottom",
|
||||
end: "bottom top",
|
||||
scrub: 1.5,
|
||||
invalidateOnRefresh: true,
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
ScrollTrigger.getAll().forEach((t) => {
|
||||
if (t.trigger === el) t.kill();
|
||||
});
|
||||
};
|
||||
}, [speed]);
|
||||
|
||||
return (
|
||||
<div ref={elementRef} className={className} style={{ willChange: "transform" }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
9. CountUp
|
||||
Animated number counter that triggers on scroll.
|
||||
============================================================ */
|
||||
interface CountUpProps {
|
||||
end: number;
|
||||
start?: number;
|
||||
duration?: number;
|
||||
decimals?: number;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
className?: string;
|
||||
triggerOnce?: boolean;
|
||||
}
|
||||
|
||||
export function CountUp({
|
||||
end,
|
||||
start = 0,
|
||||
duration = 2,
|
||||
decimals = 0,
|
||||
suffix = "",
|
||||
prefix = "",
|
||||
className = "",
|
||||
triggerOnce = false,
|
||||
}: CountUpProps) {
|
||||
const [value, setValue] = useState(start);
|
||||
const elementRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = elementRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const trigger = ScrollTrigger.create({
|
||||
trigger: el,
|
||||
start: "top 90%",
|
||||
onEnter: (self) => {
|
||||
const obj = { val: start };
|
||||
gsap.to(obj, {
|
||||
val: end,
|
||||
duration,
|
||||
ease: "power2.out",
|
||||
onUpdate: () => setValue(obj.val),
|
||||
});
|
||||
if (triggerOnce) self.kill();
|
||||
},
|
||||
onEnterBack: () => {
|
||||
if (!triggerOnce) {
|
||||
const obj = { val: start };
|
||||
gsap.to(obj, {
|
||||
val: end,
|
||||
duration,
|
||||
ease: "power2.out",
|
||||
onUpdate: () => setValue(obj.val),
|
||||
});
|
||||
}
|
||||
},
|
||||
onLeave: () => {
|
||||
if (!triggerOnce) {
|
||||
setValue(start);
|
||||
}
|
||||
},
|
||||
onLeaveBack: () => {
|
||||
if (!triggerOnce) {
|
||||
setValue(start);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => trigger?.kill();
|
||||
}, [start, end, duration, triggerOnce]);
|
||||
|
||||
return (
|
||||
<span ref={elementRef} className={className}>
|
||||
{prefix}{value.toFixed(decimals)}{suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
10. Tilt3D
|
||||
Subtle 3D perspective tilt following mouse on hover.
|
||||
============================================================ */
|
||||
interface Tilt3DProps {
|
||||
children: React.ReactNode;
|
||||
intensity?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Tilt3D({
|
||||
children,
|
||||
intensity = 8,
|
||||
className = "",
|
||||
}: Tilt3DProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width - 0.5;
|
||||
const y = (e.clientY - rect.top) / rect.height - 0.5;
|
||||
|
||||
gsap.to(el, {
|
||||
rotateY: x * intensity,
|
||||
rotateX: -y * intensity,
|
||||
duration: 0.5,
|
||||
ease: "power2.out",
|
||||
});
|
||||
}, [intensity]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
gsap.to(el, {
|
||||
rotateY: 0,
|
||||
rotateX: 0,
|
||||
duration: 0.8,
|
||||
ease: "elastic.out(1, 0.5)",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ perspective: "1000px" }}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ transformStyle: "preserve-3d", willChange: "transform" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user