"use client"; import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; import { animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react"; import { type ReactNode, useEffect, useId, useRef, useState } from "react"; import { cn } from "@/lib/utils"; const TONES = { primary: "[--liquid:var(--color-primary)] [--liquid-ink:var(--color-primary-foreground)]", success: "[--liquid:var(--color-success)] [--liquid-ink:var(--color-white)]", info: "[--liquid:var(--color-info)] [--liquid-ink:var(--color-white)]", } as const; const SIZES = { sm: 16, default: 20, lg: 24 } as const; function liquid(size: number, level: number, slosh: number, swing: number, offset: number) { const amp = size * 0.11 * Math.sin(Math.PI * Math.min(Math.max(slosh, 0), 1)) * swing; const top = -size * 0.15 + size * 1.3 * (1 - level); const phase = slosh * Math.PI * 2.2 + offset; const wavelength = size * 0.9; let d = `M -1 ${size + 1}`; for (let x = -1; x <= size + 1; x += size / 16) { d += ` L ${x.toFixed(2)} ${(top + amp * Math.sin((x / wavelength) * Math.PI * 2 + phase)).toFixed(2)}`; } return `${d} L ${size + 1} ${size + 1} Z`; } export interface LiquidCheckboxProps extends Omit { checked?: boolean; defaultChecked?: boolean; onCheckedChange?: (checked: boolean) => void; fill?: number; label?: ReactNode; description?: ReactNode; size?: keyof typeof SIZES; tone?: keyof typeof TONES; strike?: boolean; className?: string; } export function LiquidCheckbox({ checked: checkedProp, defaultChecked = false, onCheckedChange, fill = 0, label, description, size = "default", tone = "primary", strike = false, disabled, className, ...props }: LiquidCheckboxProps) { const [inner, setInner] = useState(defaultChecked); const checked = checkedProp ?? inner; const reduce = useReducedMotion(); const s = SIZES[size]; const clipId = `liquid-check-${useId().replace(/:/g, "")}`; const descriptionId = useId(); const partial = !checked && fill > 0 && fill < 1; const target = checked ? 1 : Math.min(Math.max(fill, 0), 1); const level = useMotionValue(target); const slosh = useMotionValue(1); const swing = useRef(0); const front = useTransform(() => liquid(s, level.get(), slosh.get(), swing.current, 0)); const back = useTransform(() => liquid(s, Math.min(level.get() + 0.07, 1), slosh.get(), swing.current * 0.8, Math.PI * 0.9)); useEffect(() => { const from = level.get(); if (from === target) return; if (reduce) { level.jump(target); return; } swing.current = Math.min(1, Math.abs(target - from) * 1.6); const transition = target > from ? { duration: 0.32, ease: [0.23, 1, 0.32, 1] as const } : { duration: 0.24, ease: [0.45, 0, 0.55, 1] as const }; slosh.jump(0); const a = animate(level, target, transition); const b = animate(slosh, 1, transition); return () => { a.stop(); b.stop(); }; }, [target, reduce, level, slosh]); const setChecked = (next: boolean) => { if (checkedProp === undefined) setInner(next); onCheckedChange?.(next); }; const box = ( setChecked(next)} style={{ width: s, height: s, ["--box" as string]: `${s}px` }} {...props} > ); if (!label) return box; return ( ); }