infoalloggi-monorepo/apps/infoalloggi/src/components/custom_ui/animated-button.tsx
2025-08-29 16:18:32 +02:00

127 lines
2.8 KiB
TypeScript

import { type AnimationSequence, useAnimate } from "framer-motion";
import {
type ComponentPropsWithoutRef,
forwardRef,
useImperativeHandle,
} from "react";
import { IconMatrix, type IconType } from "~/components/IconComponents";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
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}`,
{
opacity: 1,
scale: randomNumberBetween(sparklesScale[0], sparklesScale[1]),
x: randomNumberBetween(-sparklesRadius, sparklesRadius),
y: randomNumberBetween(-sparklesRadius, sparklesRadius),
},
{
at: "<",
delay: index * 0.01,
duration: 0.4,
},
]);
const sparklesFadeOut: AnimationSequence = sparkles.map((_, index) => [
`.sparkle-${index}`,
{
opacity: 0,
scale: 0,
},
{
at: "<",
duration: 0.3,
},
]);
const sparklesReset: AnimationSequence = sparkles.map((_, index) => [
`.sparkle-${index}`,
{
x: 0,
y: 0,
},
{
duration: 0.000001,
},
]);
animate([
...sparklesReset,
["button", { scale: 0.8 }, { at: "<", duration: 0.1 }],
["button", { scale: 1 }, { duration: 0.1 }],
...sparklesAnimation,
["button", { duration: 0.000001 }],
...sparklesFadeOut,
]);
};
useImperativeHandle(ref, () => ({
sparkle: Sparkles,
}));
return (
<div className="w-full" ref={scope}>
<Button
{...props}
aria-label="Animated Button"
className={cn("relative", props.className)}
>
{props.children}
<span
aria-hidden
className="pointer-events-none absolute inset-0 z-10 block"
>
{Array.from({ length: sparklesNumber }).map((_, index) => (
<IconMatrix
className={cn(
`absolute top-1/2 left-1/2 opacity-0 sparkle-${index}`,
sparklesClassName,
)}
key={index}
type={sparklesIcon}
/>
))}
</span>
</Button>
</div>
);
},
);
AnimatedButton.displayName = "AnimatedButton";