diff --git a/.env.production b/.env.production index eb897e7..acb98ca 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ -NEXT_PUBLIC_APP_VERSION=2.2.1 +NEXT_PUBLIC_APP_VERSION=2.2.2 NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500 NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0 NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com diff --git a/src/app/forms/form-elements/page.tsx b/src/app/forms/form-elements/page.tsx index ff616c7..3ab7daf 100644 --- a/src/app/forms/form-elements/page.tsx +++ b/src/app/forms/form-elements/page.tsx @@ -113,9 +113,9 @@ export default function FormElementsPage() { title="Checkbox and radio" className="space-y-5.5 !p-6.5" > - {/* - - */} + + + diff --git a/src/components/FormElements/checkbox.tsx b/src/components/FormElements/checkbox.tsx index 00e7784..12a9b24 100644 --- a/src/components/FormElements/checkbox.tsx +++ b/src/components/FormElements/checkbox.tsx @@ -1,69 +1,238 @@ +"use client"; + import { CheckIcon, XIcon } from "@/assets/icons"; import { cn } from "@/lib/utils"; -import { useId } from "react"; +import gsap from "gsap"; +import { + forwardRef, + useId, + useImperativeHandle, + useLayoutEffect, + useRef, + type InputHTMLAttributes, +} from "react"; type PropsType = { - withIcon?: "check" | "x"; + withIcon?: "check" | "x" | "none"; withBg?: boolean; label: string; name?: string; minimal?: boolean; onChange?: (e: React.ChangeEvent) => void; radius?: "default" | "md"; -}; - -export function Checkbox({ - withIcon, - label, - name, - withBg, - minimal, - onChange, - radius, -}: PropsType) { - const id = useId(); + className?: string; +} & Omit, "type">; +function prefersReducedMotion() { return ( -
- -
+ typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches ); } + +export const Checkbox = forwardRef(function Checkbox( + { + withIcon = "check", + label, + name, + withBg = true, + minimal, + onChange, + radius = "md", + className, + disabled, + id: idProp, + checked, + defaultChecked, + ...rest + }, + ref, +) { + const autoId = useId(); + const id = idProp ?? autoId; + const inputRef = useRef(null); + const boxRef = useRef(null); + const fillRef = useRef(null); + const iconRef = useRef(null); + const glowRef = useRef(null); + + useImperativeHandle(ref, () => inputRef.current as HTMLInputElement); + + const animateState = (isChecked: boolean) => { + const box = boxRef.current; + if (!box || prefersReducedMotion()) return; + + const fill = fillRef.current; + const icon = iconRef.current; + const glow = glowRef.current; + + gsap.killTweensOf([box, fill, icon, glow].filter(Boolean)); + + if (isChecked) { + const tl = gsap.timeline(); + tl.to(box, { scale: 1.14, duration: 0.1, ease: "power2.out" }).to(box, { + scale: 1, + duration: 0.4, + ease: "back.out(2.6)", + }); + if (fill) { + tl.fromTo( + fill, + { scale: 0.35, opacity: 0 }, + { scale: 1, opacity: 1, duration: 0.32, ease: "power3.out" }, + 0, + ); + } + if (glow) { + tl.fromTo( + glow, + { opacity: 0, scale: 0.75 }, + { opacity: 1, scale: 1, duration: 0.38, ease: "power2.out" }, + 0.04, + ); + } + if (icon) { + tl.fromTo( + icon, + { scale: 0, rotate: -48, opacity: 0 }, + { + scale: 1, + rotate: 0, + opacity: 1, + duration: 0.38, + ease: "back.out(3)", + }, + 0.1, + ); + } + } else { + gsap.to(box, { scale: 1, duration: 0.22, ease: "power2.out" }); + if (fill) { + gsap.to(fill, { + scale: 0.4, + opacity: 0, + duration: 0.2, + ease: "power2.in", + }); + } + if (glow) { + gsap.to(glow, { opacity: 0, scale: 0.85, duration: 0.2 }); + } + if (icon) { + gsap.to(icon, { + scale: 0, + rotate: 36, + opacity: 0, + duration: 0.18, + ease: "power2.in", + }); + } + } + }; + + useLayoutEffect(() => { + if (inputRef.current) { + animateState(inputRef.current.checked); + } + }, [checked]); + + const handleChange = (e: React.ChangeEvent) => { + animateState(e.target.checked); + onChange?.(e); + }; + + const showCheckIcon = withIcon === "check"; + const showXIcon = withIcon === "x"; + const showDot = withIcon === "none"; + + return ( +