"use client"; import { Label } from "@/components/ui/label"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import * as SwitchPrimitive from "@radix-ui/react-switch"; import gsap from "gsap"; import * as React from "react"; interface SwitchProps extends React.ComponentPropsWithoutRef { size?: "sm" | "md" | "lg"; label?: string; tooltip?: string; } const sizeVariants = { sm: { track: "h-7 w-11", thumb: { width: 20, height: 20 }, expandedWidth: 24, padding: 2, fontSize: "text-sm", offsetX: 3, }, md: { track: "h-8 w-[3.25rem]", thumb: { width: 24, height: 24 }, expandedWidth: 28, padding: 3, fontSize: "text-base", offsetX: 3, }, lg: { track: "h-10 w-16", thumb: { width: 28, height: 28 }, expandedWidth: 32, padding: 3, fontSize: "text-lg", offsetX: 3, }, }; export const Switch = React.forwardRef< React.ElementRef, SwitchProps >(({ size = "md", label, tooltip, className, ...props }, ref) => { const thumbRef = React.useRef(null); const trackRef = React.useRef(null); const id = React.useId(); const [showTooltip, setShowTooltip] = React.useState(false); const timeoutRef = React.useRef(null); const isInitializedRef = React.useRef(false); const [internalChecked, setInternalChecked] = React.useState( props.defaultChecked || false ); const config = sizeVariants[size]; const isControlled = props.checked !== undefined; const isChecked = isControlled ? props.checked : internalChecked; React.useEffect(() => { if (!thumbRef.current || !trackRef.current) return; const thumb = thumbRef.current; const track = trackRef.current; if (!isInitializedRef.current) { const trackWidth = track.offsetWidth; const thumbWidth = config.thumb.width; const maxX = trackWidth - thumbWidth - config.padding * 2; // Initial setup without animation gsap.set(thumb, { x: isChecked ? maxX : config.offsetX, width: config.thumb.width, height: config.thumb.height, }); gsap.set(track, { backgroundColor: isChecked ? "rgb(0, 0, 0)" : "rgb(209, 213, 219)", }); isInitializedRef.current = true; } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { if (!isInitializedRef.current || !thumbRef.current || !trackRef.current) return; const thumb = thumbRef.current; const track = trackRef.current; const trackWidth = track.offsetWidth; const thumbWidth = config.thumb.width; const maxX = trackWidth - thumbWidth - config.padding * 2; const tl = gsap.timeline(); tl.to(thumb, { width: config.expandedWidth, transformOrigin: isChecked ? "right" : "left", duration: 0.3, ease: "power2.out", }); tl.to( thumb, { x: isChecked ? maxX : config.offsetX, duration: 0.25, ease: "power2.inOut", }, "-=0.05" ); tl.to( track, { backgroundColor: isChecked ? "rgb(0, 0, 0)" : "rgb(209, 213, 219)", duration: 0.3, ease: "power2.inOut", }, "-=0.25" ); tl.to( thumb, { width: config.thumb.width, transformOrigin: isChecked ? "left" : "right", duration: 0.3, ease: "power2.out", }, "-=0.25" ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isChecked]); const handleCheckedChange = (checked: boolean) => { if (!isControlled) { setInternalChecked(checked); } props.onCheckedChange?.(checked); }; const handleMouseEnter = () => { if (!tooltip || props.disabled) return; timeoutRef.current = setTimeout(() => setShowTooltip(true), 500); }; const handleMouseLeave = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setShowTooltip(false); }; React.useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); const SwitchComponent = ( ); return (
{tooltip ? ( {SwitchComponent} {tooltip} ) : ( SwitchComponent )} {label && ( )}
); }); Switch.displayName = "Switch";