128 lines
3.3 KiB
TypeScript
128 lines
3.3 KiB
TypeScript
import { useAnimate, type AnimationSequence } from "framer-motion";
|
|
import { Button } from "~/components/ui/button";
|
|
import { IconMatrix, type IconType } from "~/components/IconComponents";
|
|
import { cn } from "~/lib/utils";
|
|
import {
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
type ComponentPropsWithoutRef,
|
|
} from "react";
|
|
|
|
const randomNumberBetween = (min: number, max: number) => {
|
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
};
|
|
|
|
export interface AnimatedButtonRef {
|
|
sparkle: () => void;
|
|
}
|
|
|
|
interface AnimatedButtonProps extends ComponentPropsWithoutRef<typeof Button> {
|
|
sparklesRadius?: number;
|
|
sparklesNumber?: number;
|
|
sparklesClassName?: string;
|
|
sparklesIcon?: IconType;
|
|
sparklesScale?: [number, number];
|
|
}
|
|
|
|
export const AnimatedButton = forwardRef<
|
|
AnimatedButtonRef,
|
|
AnimatedButtonProps
|
|
>(
|
|
(
|
|
{
|
|
sparklesRadius = 100,
|
|
sparklesNumber = 20,
|
|
sparklesClassName = "fill-blue-500 stroke-transparent",
|
|
sparklesIcon = "star",
|
|
sparklesScale = [1.5, 2.5],
|
|
...props
|
|
},
|
|
ref,
|
|
) => {
|
|
const [scope, animate] = useAnimate();
|
|
|
|
const Sparkles = () => {
|
|
const sparkles = Array.from({ length: sparklesNumber });
|
|
const sparklesAnimation: AnimationSequence = sparkles.map((_, index) => [
|
|
`.sparkle-${index}`,
|
|
{
|
|
x: randomNumberBetween(-sparklesRadius, sparklesRadius),
|
|
y: randomNumberBetween(-sparklesRadius, sparklesRadius),
|
|
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
|
|
opacity: 1,
|
|
},
|
|
{
|
|
duration: 0.4,
|
|
at: "<",
|
|
delay: index * 0.01,
|
|
},
|
|
]);
|
|
|
|
const sparklesFadeOut: AnimationSequence = sparkles.map((_, index) => [
|
|
`.sparkle-${index}`,
|
|
{
|
|
opacity: 0,
|
|
scale: 0,
|
|
},
|
|
{
|
|
duration: 0.3,
|
|
at: "<",
|
|
},
|
|
]);
|
|
|
|
const sparklesReset: AnimationSequence = sparkles.map((_, index) => [
|
|
`.sparkle-${index}`,
|
|
{
|
|
x: 0,
|
|
y: 0,
|
|
},
|
|
{
|
|
duration: 0.000001,
|
|
},
|
|
]);
|
|
|
|
animate([
|
|
...sparklesReset,
|
|
["button", { scale: 0.8 }, { duration: 0.1, at: "<" }],
|
|
["button", { scale: 1 }, { duration: 0.1 }],
|
|
...sparklesAnimation,
|
|
["button", { duration: 0.000001 }],
|
|
...sparklesFadeOut,
|
|
]);
|
|
};
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
sparkle: Sparkles,
|
|
}));
|
|
|
|
return (
|
|
<div ref={scope} className="w-full">
|
|
<Button
|
|
{...props}
|
|
className={cn("relative", props.className)}
|
|
aria-label="Animated Button"
|
|
>
|
|
{props.children}
|
|
<span
|
|
aria-hidden
|
|
className="pointer-events-none absolute inset-0 z-10 block"
|
|
>
|
|
{Array.from({ length: sparklesNumber }).map((_, index) => (
|
|
<IconMatrix
|
|
type={sparklesIcon}
|
|
className={cn(
|
|
// eslint-disable-next-line better-tailwindcss/no-unregistered-classes
|
|
`absolute top-1/2 left-1/2 opacity-0 sparkle-${index}`,
|
|
sparklesClassName,
|
|
)}
|
|
key={index}
|
|
/>
|
|
))}
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
);
|
|
|
|
AnimatedButton.displayName = "AnimatedButton";
|