menu.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. "use client";
  2. import type { Variants } from "motion/react";
  3. import { motion, useAnimation } from "motion/react";
  4. import type { HTMLAttributes } from "react";
  5. import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
  6. import { cn } from "@/lib/utils";
  7. export interface MenuIconHandle {
  8. startAnimation: () => void;
  9. stopAnimation: () => void;
  10. }
  11. interface MenuIconProps extends HTMLAttributes<HTMLDivElement> {
  12. size?: number;
  13. }
  14. const LINE_VARIANTS: Variants = {
  15. normal: {
  16. rotate: 0,
  17. y: 0,
  18. opacity: 1,
  19. },
  20. animate: (custom: number) => ({
  21. rotate: custom === 1 ? 45 : custom === 3 ? -45 : 0,
  22. y: custom === 1 ? 6 : custom === 3 ? -6 : 0,
  23. opacity: custom === 2 ? 0 : 1,
  24. transition: {
  25. type: "spring",
  26. stiffness: 260,
  27. damping: 20,
  28. },
  29. }),
  30. };
  31. const MenuIcon = forwardRef<MenuIconHandle, MenuIconProps>(
  32. ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
  33. const controls = useAnimation();
  34. const isControlledRef = useRef(false);
  35. useImperativeHandle(ref, () => {
  36. isControlledRef.current = true;
  37. return {
  38. startAnimation: () => controls.start("animate"),
  39. stopAnimation: () => controls.start("normal"),
  40. };
  41. });
  42. const handleMouseEnter = useCallback(
  43. (e: React.MouseEvent<HTMLDivElement>) => {
  44. if (isControlledRef.current) {
  45. onMouseEnter?.(e);
  46. } else {
  47. controls.start("animate");
  48. }
  49. },
  50. [controls, onMouseEnter]
  51. );
  52. const handleMouseLeave = useCallback(
  53. (e: React.MouseEvent<HTMLDivElement>) => {
  54. if (isControlledRef.current) {
  55. onMouseLeave?.(e);
  56. } else {
  57. controls.start("normal");
  58. }
  59. },
  60. [controls, onMouseLeave]
  61. );
  62. return (
  63. <div
  64. className={cn(className)}
  65. onMouseEnter={handleMouseEnter}
  66. onMouseLeave={handleMouseLeave}
  67. {...props}
  68. >
  69. <svg
  70. fill="none"
  71. height={size}
  72. stroke="currentColor"
  73. strokeLinecap="round"
  74. strokeLinejoin="round"
  75. strokeWidth="2"
  76. viewBox="0 0 24 24"
  77. width={size}
  78. xmlns="http://www.w3.org/2000/svg"
  79. >
  80. <motion.line
  81. animate={controls}
  82. custom={1}
  83. initial="normal"
  84. variants={LINE_VARIANTS}
  85. x1="4"
  86. x2="20"
  87. y1="6"
  88. y2="6"
  89. />
  90. <motion.line
  91. animate={controls}
  92. custom={2}
  93. initial="normal"
  94. variants={LINE_VARIANTS}
  95. x1="4"
  96. x2="20"
  97. y1="12"
  98. y2="12"
  99. />
  100. <motion.line
  101. animate={controls}
  102. custom={3}
  103. initial="normal"
  104. variants={LINE_VARIANTS}
  105. x1="4"
  106. x2="20"
  107. y1="18"
  108. y2="18"
  109. />
  110. </svg>
  111. </div>
  112. );
  113. }
  114. );
  115. MenuIcon.displayName = "MenuIcon";
  116. export { MenuIcon };