shield-check.tsx 2.7 KB

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