layout-grid.tsx133 lines · main
1'use client';
2
3import { motion, useAnimation } from 'motion/react';
4import type { HTMLAttributes } from 'react';
5import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
6
7import { cn } from '@/lib/utils';
8
9export interface LayoutGridIconHandle {
10 startAnimation: () => void;
11 stopAnimation: () => void;
12}
13
14interface LayoutGridIconProps extends HTMLAttributes<HTMLDivElement> {
15 size?: number;
16}
17
18const LayoutGridIcon = forwardRef<LayoutGridIconHandle, LayoutGridIconProps>(
19 ({ onMouseEnter, onMouseLeave, className, size = 18, ...props }, ref) => {
20 const controls = useAnimation();
21 const isControlledRef = useRef(false);
22
23 useImperativeHandle(ref, () => {
24 isControlledRef.current = true;
25 return {
26 startAnimation: () => controls.start('animate'),
27 stopAnimation: () => controls.start('normal'),
28 };
29 });
30
31 const handleMouseEnter = useCallback(
32 (e: React.MouseEvent<HTMLDivElement>) => {
33 if (isControlledRef.current) {
34 onMouseEnter?.(e);
35 } else {
36 controls.start('animate');
37 }
38 },
39 [controls, onMouseEnter],
40 );
41
42 const handleMouseLeave = useCallback(
43 (e: React.MouseEvent<HTMLDivElement>) => {
44 if (isControlledRef.current) {
45 onMouseLeave?.(e);
46 } else {
47 controls.start('normal');
48 }
49 },
50 [controls, onMouseLeave],
51 );
52
53 const tileTransition = { type: 'spring' as const, stiffness: 300, damping: 20 };
54
55 return (
56 <div
57 className={cn(className)}
58 onMouseEnter={handleMouseEnter}
59 onMouseLeave={handleMouseLeave}
60 {...props}
61 >
62 <svg
63 fill="none"
64 height={size}
65 stroke="currentColor"
66 strokeLinecap="round"
67 strokeLinejoin="round"
68 strokeWidth="2"
69 viewBox="0 0 24 24"
70 width={size}
71 xmlns="http://www.w3.org/2000/svg"
72 >
73 <motion.rect
74 x="3"
75 y="3"
76 width="7"
77 height="7"
78 rx="1"
79 animate={controls}
80 transition={tileTransition}
81 variants={{
82 normal: { translateX: 0, translateY: 0 },
83 animate: { translateX: -1, translateY: -1 },
84 }}
85 />
86 <motion.rect
87 x="14"
88 y="3"
89 width="7"
90 height="7"
91 rx="1"
92 animate={controls}
93 transition={{ ...tileTransition, delay: 0.04 }}
94 variants={{
95 normal: { translateX: 0, translateY: 0 },
96 animate: { translateX: 1, translateY: -1 },
97 }}
98 />
99 <motion.rect
100 x="14"
101 y="14"
102 width="7"
103 height="7"
104 rx="1"
105 animate={controls}
106 transition={{ ...tileTransition, delay: 0.08 }}
107 variants={{
108 normal: { translateX: 0, translateY: 0 },
109 animate: { translateX: 1, translateY: 1 },
110 }}
111 />
112 <motion.rect
113 x="3"
114 y="14"
115 width="7"
116 height="7"
117 rx="1"
118 animate={controls}
119 transition={{ ...tileTransition, delay: 0.12 }}
120 variants={{
121 normal: { translateX: 0, translateY: 0 },
122 animate: { translateX: -1, translateY: 1 },
123 }}
124 />
125 </svg>
126 </div>
127 );
128 },
129);
130
131LayoutGridIcon.displayName = 'LayoutGridIcon';
132
133export { LayoutGridIcon };