volume

PreviousNext
Docs
lucide-animatedui

Preview

Loading preview…
volume.tsx
'use client';

import type { HTMLAttributes } from 'react';
import {
  forwardRef,
  Fragment,
  useCallback,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import { AnimatePresence, motion } from 'motion/react';

import { cn } from '@/lib/utils';

export interface VolumeIconHandle {
  startAnimation: () => void;
  stopAnimation: () => void;
}

interface VolumeIconProps extends HTMLAttributes<HTMLDivElement> {
  size?: number;
}

const VolumeIcon = forwardRef<VolumeIconHandle, VolumeIconProps>(
  ({ onMouseEnter, onMouseLeave, className, size = 28, ...props }, ref) => {
    const [isHovered, setIsHovered] = useState(false);
    const isControlledRef = useRef(false);

    useImperativeHandle(ref, () => {
      isControlledRef.current = true;

      return {
        startAnimation: () => setIsHovered(true),
        stopAnimation: () => setIsHovered(false),
      };
    });

    const handleMouseEnter = useCallback(
      (e: React.MouseEvent<HTMLDivElement>) => {
        if (!isControlledRef.current) {
          setIsHovered(true);
        } else {
          onMouseEnter?.(e);
        }
      },
      [onMouseEnter]
    );

    const handleMouseLeave = useCallback(
      (e: React.MouseEvent<HTMLDivElement>) => {
        if (!isControlledRef.current) {
          setIsHovered(false);
        } else {
          onMouseLeave?.(e);
        }
      },
      [onMouseLeave]
    );

    return (
      <div
        className={cn(className)}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        {...props}
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width={size}
          height={size}
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z" />
          <AnimatePresence mode="wait" initial={false}>
            {isHovered ? (
              <Fragment key="volume-icon-active">
                <motion.path
                  d="M16 9a5 5 0 0 1 0 6"
                  animate={{ opacity: 1, transition: { delay: 0.1 } }}
                  initial={{ opacity: 0 }}
                  exit={{ opacity: 0 }}
                />
                <motion.path
                  d="M19.364 18.364a9 9 0 0 0 0-12.728"
                  animate={{ opacity: 1, transition: { delay: 0.2 } }}
                  initial={{ opacity: 0 }}
                  exit={{ opacity: 0 }}
                />
              </Fragment>
            ) : (
              <Fragment key="volume-icon-inactive">
                <motion.line
                  x1="22"
                  x2="16"
                  y1="9"
                  y2="15"
                  animate={{
                    pathLength: [0, 1],
                    opacity: [0, 1],
                    transition: { delay: 0.1 },
                  }}
                  initial={{ pathLength: 1, opacity: 1 }}
                  exit={{ pathLength: 1, opacity: 1 }}
                />
                <motion.line
                  x1="16"
                  x2="22"
                  y1="9"
                  y2="15"
                  animate={{
                    pathLength: [0, 1],
                    opacity: [0, 1],
                    transition: { delay: 0.2 },
                  }}
                  initial={{ pathLength: 1, opacity: 1 }}
                  exit={{ pathLength: 1, opacity: 1 }}
                />
              </Fragment>
            )}
          </AnimatePresence>
        </svg>
      </div>
    );
  }
);

VolumeIcon.displayName = 'VolumeIcon';

export { VolumeIcon };

Installation

npx shadcn@latest add @lucide-animated/volume

Usage

import { Volume } from "@/components/ui/volume"
<Volume />