"use client"; import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; import { motion, useMotionValue, useReducedMotion, useSpring, useTransform, useVelocity } from "motion/react"; import { type ReactNode, useEffect, useId, useState } from "react"; import { cn } from "@/lib/utils"; const SIZES = { sm: { w: 34, h: 20, pad: 2, blur: 3 }, default: { w: 44, h: 26, pad: 3, blur: 3.5 }, lg: { w: 58, h: 34, pad: 3, blur: 4.5 }, } as const; const TONES = { primary: "data-checked:bg-primary", success: "data-checked:bg-success", info: "data-checked:bg-info", destructive: "data-checked:bg-destructive", } as const; export interface LiquidSwitchProps extends Omit { checked?: boolean; defaultChecked?: boolean; onCheckedChange?: (checked: boolean) => void; size?: keyof typeof SIZES; tone?: keyof typeof TONES; label?: ReactNode; description?: ReactNode; } export function LiquidSwitch({ checked: checkedProp, defaultChecked = false, onCheckedChange, size = "default", tone = "primary", label, description, className, disabled, ...props }: LiquidSwitchProps) { const [inner, setInner] = useState(defaultChecked); const checked = checkedProp ?? inner; const reduce = useReducedMotion(); const filterId = `liquid-switch-${useId().replace(/:/g, "")}`; const descriptionId = useId(); const { w, h, pad, blur } = SIZES[size]; const thumb = h - pad * 2; const travel = w - h; const target = useMotionValue(checked ? travel : 0); const x = useSpring(target, { stiffness: 560, damping: 34, mass: 0.9 }); const trailA = useSpring(target, { stiffness: 300, damping: 26 }); const trailB = useSpring(target, { stiffness: 170, damping: 20 }); const velocity = useVelocity(x); const scaleX = useTransform(velocity, [-1400, 0, 1400], [1.3, 1, 1.3], { clamp: true }); const scaleY = useTransform(velocity, [-1400, 0, 1400], [0.86, 1, 0.86], { clamp: true }); useEffect(() => { const next = checked ? travel : 0; if (reduce) { for (const value of [target, x, trailA, trailB]) value.jump(next); } else { target.set(next); } }, [checked, travel, reduce, target, x, trailA, trailB]); const setChecked = (next: boolean) => { if (checkedProp === undefined) setInner(next); onCheckedChange?.(next); }; const blob = cn( "absolute top-1/2 left-0 rounded-full bg-white transition-colors duration-150", tone === "primary" && "group-data-checked/switch:bg-primary-foreground", ); const control = ( ); if (!label) return control; return ( ); }