ai-icon-animation.tsx128 lines · main
1'use client'
2
3import { motion, useMotionValue, useSpring } from 'framer-motion'
4import { memo, useRef, useState } from 'react'
5
6import { cn } from '../../lib/utils'
7
8interface AiIconAnimationProps {
9 size?: number
10 loading?: boolean
11 className?: string
12 allowHoverEffect?: boolean
13}
14
15const AiIconAnimationComponent = ({
16 size = 24,
17 loading = false,
18 className,
19 allowHoverEffect = false,
20}: AiIconAnimationProps) => {
21 const strokeWidth = Math.max(1.5, size / 46) // Ensure minimum stroke width of 1.5
22 const containerRef = useRef<HTMLDivElement>(null)
23 const [isHovering, setIsHovering] = useState(false)
24
25 const x = useMotionValue(0)
26 const y = useMotionValue(0)
27
28 const springX = useSpring(x, { stiffness: 300, damping: 30 })
29 const springY = useSpring(y, { stiffness: 300, damping: 30 })
30
31 const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
32 if (!allowHoverEffect) return
33 if (!containerRef.current) return
34
35 const rect = containerRef.current.getBoundingClientRect()
36 const centerX = rect.left + rect.width / 2
37 const centerY = rect.top + rect.height / 2
38
39 const mouseX = event.clientX - centerX
40 const mouseY = event.clientY - centerY
41
42 x.set(mouseX / 5)
43 y.set(mouseY / 5)
44 }
45
46 const outerAnimate = loading ? { rotate: 360 } : { rotate: isHovering ? 10 : 0 }
47
48 const outerTransition = loading
49 ? {
50 type: 'spring' as const,
51 stiffness: 60,
52 damping: 10,
53 repeat: Infinity,
54 repeatType: 'loop' as const,
55 }
56 : { type: 'spring' as const, stiffness: 300, damping: 30 }
57
58 const innerAnimate = loading ? { scale: [1, 1.1, 1] } : { scale: isHovering ? 1.1 : 1 }
59
60 const innerTransition = loading
61 ? { duration: 2, repeat: Infinity, ease: 'easeInOut' as const, repeatType: 'loop' as const }
62 : { type: 'spring' as const, stiffness: 300, damping: 30 }
63
64 return (
65 <div
66 className={cn('text-brand-600 flex justify-center items-center relative', className)}
67 style={{ width: size, height: size, position: 'relative' }}
68 >
69 <div
70 ref={containerRef}
71 className="absolute flex items-center justify-center"
72 style={{
73 width: size * 2,
74 height: size * 2,
75 left: -size / 2,
76 top: -size / 2,
77 }}
78 onMouseMove={handleMouseMove}
79 onMouseEnter={() => setIsHovering(true)}
80 onMouseLeave={() => {
81 setIsHovering(false)
82 x.set(0)
83 y.set(0)
84 }}
85 />
86
87 <motion.svg
88 width={size}
89 height={size}
90 viewBox="0 0 46 46"
91 fill="none"
92 xmlns="http://www.w3.org/2000/svg"
93 >
94 <motion.path
95 fillRule="evenodd"
96 clipRule="evenodd"
97 d="M23 1.78677L44.2132 23L23 44.2132L1.7868 23L23 1.78677ZM23 0.372559L23.7071 1.07967L44.9203 22.2929L45.6274 23L44.9203 23.7071L23.7071 44.9203L23 45.6274L22.2929 44.9203L1.07969 23.7071L0.372583 23L1.07969 22.2929L22.2929 1.07967L23 0.372559Z"
98 fill="none"
99 stroke="currentColor"
100 strokeWidth={strokeWidth}
101 initial={{ rotate: 0 }}
102 animate={outerAnimate}
103 transition={outerTransition}
104 style={{ transformBox: 'view-box', transformOrigin: 'center' }}
105 />
106 <motion.path
107 fillRule="evenodd"
108 clipRule="evenodd"
109 d="M30 23C30 26.866 26.866 30 23 30C19.134 30 16 26.866 16 23C16 19.134 19.134 16 23 16C26.866 16 30 19.134 30 23ZM31 23C31 27.4183 27.4183 31 23 31C18.5817 31 15 27.4183 15 23C15 18.5817 18.5817 15 23 15C27.4183 15 31 18.5817 31 23Z"
110 fill="none"
111 stroke="currentColor"
112 strokeWidth={strokeWidth}
113 initial={{ scale: 1 }}
114 animate={innerAnimate}
115 style={{
116 x: springX,
117 y: springY,
118 transformBox: 'view-box',
119 transformOrigin: 'center',
120 }}
121 transition={innerTransition}
122 />
123 </motion.svg>
124 </div>
125 )
126}
127
128export const AiIconAnimation = memo(AiIconAnimationComponent)