search.tsx 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. "use client";
  2. import { motion, useAnimation } from "motion/react";
  3. import type { HTMLAttributes } from "react";
  4. import { forwardRef, useCallback, useImperativeHandle, useRef } from "react";
  5. import { cn } from "@/lib/utils";
  6. export interface SearchIconHandle {
  7. startAnimation: () => void;
  8. stopAnimation: () => void;
  9. }
  10. interface SearchIconProps extends HTMLAttributes<HTMLDivElement> {
  11. size?: number;
  12. }
  13. const SearchIcon = forwardRef<SearchIconHandle, SearchIconProps>(
  14. ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
  15. const controls = useAnimation();
  16. const isControlledRef = useRef(false);
  17. useImperativeHandle(ref, () => {
  18. isControlledRef.current = true;
  19. return {
  20. startAnimation: () => controls.start("animate"),
  21. stopAnimation: () => controls.start("normal"),
  22. };
  23. });
  24. const handleMouseEnter = useCallback(
  25. (e: React.MouseEvent<HTMLDivElement>) => {
  26. if (isControlledRef.current) {
  27. onMouseEnter?.(e);
  28. } else {
  29. controls.start("animate");
  30. }
  31. },
  32. [controls, onMouseEnter]
  33. );
  34. const handleMouseLeave = useCallback(
  35. (e: React.MouseEvent<HTMLDivElement>) => {
  36. if (isControlledRef.current) {
  37. onMouseLeave?.(e);
  38. } else {
  39. controls.start("normal");
  40. }
  41. },
  42. [controls, onMouseLeave]
  43. );
  44. return (
  45. <div
  46. className={cn(className)}
  47. onMouseEnter={handleMouseEnter}
  48. onMouseLeave={handleMouseLeave}
  49. {...props}
  50. >
  51. <motion.svg
  52. animate={controls}
  53. fill="none"
  54. height={size}
  55. stroke="currentColor"
  56. strokeLinecap="round"
  57. strokeLinejoin="round"
  58. strokeWidth="2"
  59. transition={{
  60. duration: 1,
  61. bounce: 0.3,
  62. }}
  63. variants={{
  64. normal: { x: 0, y: 0 },
  65. animate: {
  66. x: [0, 0, -3, 0],
  67. y: [0, -4, 0, 0],
  68. },
  69. }}
  70. viewBox="0 0 24 24"
  71. width={size}
  72. xmlns="http://www.w3.org/2000/svg"
  73. >
  74. <circle cx="11" cy="11" r="8" />
  75. <path d="m21 21-4.3-4.3" />
  76. </motion.svg>
  77. </div>
  78. );
  79. }
  80. );
  81. SearchIcon.displayName = "SearchIcon";
  82. export { SearchIcon };