83 lines
3.3 KiB
TypeScript
83 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
|
|
import type { MouseEvent, PointerEvent, ReactNode } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type PhysicsButtonProps = {
|
|
href: string;
|
|
children: ReactNode;
|
|
className?: string;
|
|
external?: boolean;
|
|
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
};
|
|
|
|
export default function PhysicsButton({ href, children, className, external = false, onClick }: PhysicsButtonProps) {
|
|
const pullX = useMotionValue(0);
|
|
const pullY = useMotionValue(0);
|
|
const press = useMotionValue(0);
|
|
const x = useSpring(pullX, { stiffness: 190, damping: 13, mass: 0.42 });
|
|
const y = useSpring(pullY, { stiffness: 190, damping: 13, mass: 0.42 });
|
|
const pressed = useSpring(press, { stiffness: 520, damping: 18, mass: 0.35 });
|
|
const pointerScaleX = useTransform(x, [-30, 0, 30], [1.18, 1, 1.18]);
|
|
const pointerScaleY = useTransform(y, [-18, 0, 18], [0.9, 1, 0.9]);
|
|
const tapScaleX = useTransform(pressed, [0, 1], [1, 1.24]);
|
|
const tapScaleY = useTransform(pressed, [0, 1], [1, 0.82]);
|
|
const scaleX = useTransform([pointerScaleX, tapScaleX], ([pointerValue, tapValue]) => (pointerValue as number) * (tapValue as number));
|
|
const scaleY = useTransform([pointerScaleY, tapScaleY], ([pointerValue, tapValue]) => (pointerValue as number) * (tapValue as number));
|
|
const rotate = useTransform(x, [-30, 0, 30], [-2.2, 0, 2.2]);
|
|
const glowX = useTransform(x, (value) => value * 0.65);
|
|
const glowY = useTransform(y, (value) => value * 0.65);
|
|
|
|
function handleMove(event: PointerEvent<HTMLAnchorElement>) {
|
|
if (event.pointerType === "touch") return;
|
|
|
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
const localX = event.clientX - bounds.left - bounds.width / 2;
|
|
const localY = event.clientY - bounds.top - bounds.height / 2;
|
|
|
|
const xLimit = 30;
|
|
const yLimit = 18;
|
|
const strength = 0.42;
|
|
|
|
pullX.set(Math.max(-xLimit, Math.min(xLimit, localX * strength)));
|
|
pullY.set(Math.max(-yLimit, Math.min(yLimit, localY * strength)));
|
|
}
|
|
|
|
function release() {
|
|
pullX.set(0);
|
|
pullY.set(0);
|
|
press.set(0);
|
|
}
|
|
|
|
return (
|
|
<motion.a
|
|
href={href}
|
|
target={external ? "_blank" : undefined}
|
|
rel={external ? "noreferrer" : undefined}
|
|
onClick={onClick}
|
|
onPointerMove={handleMove}
|
|
onPointerLeave={release}
|
|
onPointerCancel={release}
|
|
onPointerDown={(event) => {
|
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
handleMove(event);
|
|
press.set(1);
|
|
pullY.set(event.pointerType === "touch" ? 10 : 12);
|
|
}}
|
|
onPointerUp={release}
|
|
onLostPointerCapture={release}
|
|
whileHover={{ scale: 1.035 }}
|
|
style={{ x, y, scaleX, scaleY, rotate }}
|
|
transition={{ type: "spring", stiffness: 520, damping: 18, mass: 0.45 }}
|
|
className={cn("premium-glass group relative inline-flex touch-manipulation select-none items-center justify-center overflow-hidden will-change-transform", className)}
|
|
>
|
|
<motion.span
|
|
className="pointer-events-none absolute inset-0 rounded-full bg-white/15 opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-100"
|
|
style={{ x: glowX, y: glowY }}
|
|
/>
|
|
<span className="relative z-10 inline-flex items-center justify-center gap-2">{children}</span>
|
|
</motion.a>
|
|
);
|
|
}
|