"use client";
import React, { memo, useMemo } from "react";
import { motion, Transition, Variants } from "framer-motion";
import { cn } from "@/lib/utils";
interface PuzzleProps {
className?: string;
}
// Animation constants - defined outside component to prevent recreation
const ANIMATE_IN_DURATION = 2.1;
const HOLD_DURATION = 3.0;
const ANIMATE_OUT_DURATION = 2.0;
const TOTAL_DURATION =
ANIMATE_IN_DURATION + HOLD_DURATION + ANIMATE_OUT_DURATION;
// Shared transition config for better performance
const createTransition = (times: number[]): Transition => ({
duration: TOTAL_DURATION,
times,
ease: [0.25, 0.1, 0.25, 1], // Custom cubic-bezier for smoother animation
repeat: Infinity,
repeatType: "loop" as const,
});
export const Puzzle = memo(({ className }: PuzzleProps) => {
// Memoize animation variants to prevent recreation on each render
const pieceVariants = useMemo(() => {
const topLeft: Variants = {
initial: { opacity: 0, x: -100, y: -100, rotate: -15 },
animate: {
opacity: [0, 1, 1, 1, 0],
x: [-100, 0, 0, 0, -100],
y: [-100, 0, 0, 0, -100],
rotate: [-15, 0, 0, 0, -15],
},
};
const topRight: Variants = {
initial: { opacity: 0, x: 100, y: -100, rotate: 15 },
animate: {
opacity: [0, 0, 1, 1, 0],
x: [100, 100, 0, 0, 100],
y: [-100, -100, 0, 0, -100],
rotate: [15, 15, 0, 0, 15],
},
};
const bottomLeft: Variants = {
initial: { opacity: 0, x: -100, y: 100, rotate: -15 },
animate: {
opacity: [0, 0, 1, 1, 0],
x: [-100, -100, 0, 0, -100],
y: [100, 100, 0, 0, 100],
rotate: [-15, -15, 0, 0, -15],
},
};
const bottomRight: Variants = {
initial: { opacity: 0, x: 100, y: 100, rotate: 15 },
animate: {
opacity: [0, 0, 1, 1, 0],
x: [100, 100, 0, 0, 100],
y: [100, 100, 0, 0, 100],
rotate: [15, 15, 0, 0, 15],
},
};
return { topLeft, topRight, bottomLeft, bottomRight };
}, []);
// Memoize transitions to prevent recreation
const transitions = useMemo(
() => ({
topLeft: createTransition([0, 0.1, 0.296, 0.718, 1]),
topRight: createTransition([0, 0.15, 0.2, 0.718, 1]),
bottomLeft: createTransition([0, 0.2, 0.25, 0.718, 1]),
bottomRight: createTransition([0, 0.25, 0.296, 0.718, 1]),
}),
[]
);
return (
<div className={cn("flex items-center justify-center p-8", className)}>
<div className="relative w-full max-w-xs aspect-square">
{/* Puzzle piece container */}
<div className="relative w-full h-full">
{/* Top-left puzzle piece - First animation */}
<motion.div
variants={pieceVariants.topLeft}
initial="initial"
animate="animate"
transition={transitions.topLeft}
className="absolute top-0 left-0 w-1/2 h-1/2 bg-neutral-700
z-20 rounded-tl-3xl border-t-2 border-l-2 border-neutral-500"
style={{ willChange: "transform, opacity" }}
>
{/* Right connector (male) */}
<div
className="absolute -right-6 top-1/2 -translate-y-1/2 w-12 h-12 bg-neutral-500
rounded-full z-30"
/>
<div className="absolute -right-5 top-1/2 -translate-y-1/2 w-12 h-12 bg-neutral-700 rounded-full z-40" />
{/* Bottom connector (male) */}
<div
className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-12 h-12 bg-fd-background
rounded-full z-30"
/>
</motion.div>
{/* Top-right puzzle piece - Second animation */}
<motion.div
variants={pieceVariants.topRight}
initial="initial"
animate="animate"
transition={transitions.topRight}
className="absolute top-0 right-0 w-1/2 h-1/2 bg-neutral-600
rounded-tr-3xl border-r-2 border-t-2 border-neutral-500"
style={{ willChange: "transform, opacity" }}
>
{/* Left connector (female) */}
<div className="absolute -left-6 top-1/2 -translate-y-1/2 w-12 h-12 bg-fd-background rounded-full z-10"></div>
{/* Bottom connector (male) */}
<div className="absolute -bottom-1 right-1/2 translate-x-1/2 translate-y-1/2 w-12 h-12 bg-fd-background rounded-full z-20" />
<div className="absolute bottom-0 right-1/2 translate-x-1/2 translate-y-1/2 w-12 h-12 bg-neutral-500 rounded-full z-10" />
</motion.div>
{/* Bottom-left puzzle piece - Third animation */}
<motion.div
variants={pieceVariants.bottomLeft}
initial="initial"
animate="animate"
transition={transitions.bottomLeft}
className="absolute bottom-0 z-20 left-0 w-1/2 h-1/2 bg-neutral-600 rounded-bl-3xl border-l-2 border-b-2 border-neutral-500"
style={{ willChange: "transform, opacity" }}
>
{/* Top connector (female) */}
<div className="absolute -top-1 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-neutral-500 rounded-full z-20" />
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-neutral-600 rounded-full z-20" />
{/** Right connector (male) */}
<div className="absolute -right-5 bottom-1/2 translate-y-1/2 w-12 h-12 bg-fd-background rounded-full z-40" />
</motion.div>
{/* Bottom-right puzzle piece - Fourth animation */}
<motion.div
variants={pieceVariants.bottomRight}
initial="initial"
animate="animate"
transition={transitions.bottomRight}
className="absolute bottom-0 z-30 right-0 w-1/2 h-1/2 bg-neutral-700 rounded-br-3xl border-r-2 border-b-2 border-neutral-500"
style={{ willChange: "transform, opacity" }}
>
{/* Top connector (female) */}
<div className="absolute top-1 right-1/2 translate-x-1/2 -translate-y-1/2 w-12 h-12 bg-neutral-700 rounded-full z-20" />
{/** Left connector (female) */}
<div className="absolute -left-6 bottom-1/2 translate-y-1/2 w-12 h-12 bg-neutral-700 rounded-full z-20" />
<div className="absolute -left-7 bottom-1/2 translate-y-1/2 w-12 h-12 bg-neutral-500 rounded-full z-10" />
</motion.div>
</div>
</div>
</div>
);
});
Puzzle.displayName = "Puzzle";
export default Puzzle;