circle-check.tsx 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  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 CircleCheckIconHandle {
  8. startAnimation: () => void;
  9. stopAnimation: () => void;
  10. }
  11. interface CircleCheckIconProps extends HTMLAttributes<HTMLDivElement> {
  12. size?: number;
  13. }
  14. const PATH_VARIANTS: Variants = {
  15. normal: {
  16. opacity: 1,
  17. pathLength: 1,
  18. transition: {
  19. duration: 0.3,
  20. opacity: { duration: 0.1 },
  21. },
  22. },
  23. animate: {
  24. opacity: [0, 1],
  25. pathLength: [0, 1],
  26. transition: {
  27. duration: 0.4,
  28. opacity: { duration: 0.1 },
  29. },
  30. },
  31. };
  32. const CircleCheckIcon = forwardRef<CircleCheckIconHandle, CircleCheckIconProps>(
  33. ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
  34. const controls = useAnimation();
  35. const isControlledRef = useRef(false);
  36. useImperativeHandle(ref, () => {
  37. isControlledRef.current = true;
  38. return {
  39. startAnimation: () => controls.start("animate"),
  40. stopAnimation: () => controls.start("normal"),
  41. };
  42. });
  43. const handleMouseEnter = useCallback(
  44. (e: React.MouseEvent<HTMLDivElement>) => {
  45. if (isControlledRef.current) {
  46. onMouseEnter?.(e);
  47. } else {
  48. controls.start("animate");
  49. }
  50. },
  51. [controls, onMouseEnter]
  52. );
  53. const handleMouseLeave = useCallback(
  54. (e: React.MouseEvent<HTMLDivElement>) => {
  55. if (isControlledRef.current) {
  56. onMouseLeave?.(e);
  57. } else {
  58. controls.start("normal");
  59. }
  60. },
  61. [controls, onMouseLeave]
  62. );
  63. return (
  64. <div
  65. className={cn(className)}
  66. onMouseEnter={handleMouseEnter}
  67. onMouseLeave={handleMouseLeave}
  68. {...props}
  69. >
  70. <svg
  71. fill="none"
  72. height={size}
  73. stroke="currentColor"
  74. strokeLinecap="round"
  75. strokeLinejoin="round"
  76. strokeWidth="2"
  77. viewBox="0 0 24 24"
  78. width={size}
  79. xmlns="http://www.w3.org/2000/svg"
  80. >
  81. <circle cx="12" cy="12" r="10" />
  82. <motion.path
  83. animate={controls}
  84. d="m9 12 2 2 4-4"
  85. initial="normal"
  86. variants={PATH_VARIANTS}
  87. />
  88. </svg>
  89. </div>
  90. );
  91. }
  92. );
  93. CircleCheckIcon.displayName = "CircleCheckIcon";
  94. export { CircleCheckIcon };