Files
New-Optic/components/PhysicsButton.tsx

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>
);
}