ai-icon-animation.tsx128 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { motion, useMotionValue, useSpring } from 'framer-motion' |
| 4 | import { memo, useRef, useState } from 'react' |
| 5 | |
| 6 | import { cn } from '../../lib/utils' |
| 7 | |
| 8 | interface AiIconAnimationProps { |
| 9 | size?: number |
| 10 | loading?: boolean |
| 11 | className?: string |
| 12 | allowHoverEffect?: boolean |
| 13 | } |
| 14 | |
| 15 | const 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 | |
| 128 | export const AiIconAnimation = memo(AiIconAnimationComponent) |