1"use client";
2
3import React, { useState, useId } from "react";
4
5interface CheckboxProps {
6 checked?: boolean;
7 defaultChecked?: boolean;
8 onChange?: (checked: boolean) => void;
9 label?: string;
10 description?: string;
11 disabled?: boolean;
12 indeterminate?: boolean;
13 className?: string;
14}
15
16export function Checkbox({
17 checked,
18 defaultChecked = false,
19 onChange,
20 label,
21 description,
22 disabled = false,
23 indeterminate = false,
24 className = "",
25}: CheckboxProps) {
26 const [internal, setInternal] = useState(defaultChecked);
27 const uid = useId();
28 const isChecked = checked !== undefined ? checked : internal;
29
30 const toggle = () => {
31 if (disabled) return;
32 const next = !isChecked;
33 setInternal(next);
34 onChange?.(next);
35 };
36
37 const active = isChecked || indeterminate;
38
39 return (
40 <div
41 className={[
42 "flex items-center justify-between gap-4 select-none",
43 disabled ? "opacity-40 pointer-events-none" : "cursor-pointer",
44 className,
45 ].join(" ")}
46 onClick={toggle}
47 >
48 {(label || description) && (
49 <div className="flex flex-col gap-0.5 min-w-0">
50 {label && (
51 <span id={`${uid}-label`} className="text-[13px] font-medium text-neutral-900 leading-snug">{label}</span>
52 )}
53 {description && (
54 <span id={`${uid}-desc`} className="text-[12px] text-neutral-500 leading-relaxed">{description}</span>
55 )}
56 </div>
57 )}
58
59 {/* Clicks bubble to the row, so the control itself has no onClick */}
60 <button
61 type="button"
62 role="checkbox"
63 aria-checked={indeterminate ? "mixed" : isChecked}
64 aria-labelledby={label ? `${uid}-label` : undefined}
65 aria-describedby={description ? `${uid}-desc` : undefined}
66 disabled={disabled}
67 className={[
68 "shrink-0 w-[22px] h-[22px] rounded-[7px] flex items-center justify-center cursor-pointer",
69 "transition-[box-shadow,transform,scale] duration-200 ease-out active:scale-90",
70 ].join(" ")}
71 style={{
72 background: active ? "var(--face-dark)" : "var(--field)",
73 boxShadow: active ? "var(--face-dark-shadow)" : "var(--field-shadow)",
74 }}
75 >
76 {active && (
77 indeterminate ? (
78 <span className="animate-checkbox-in">
79 <svg width="10" height="2" viewBox="0 0 10 2" fill="none">
80 <rect width="10" height="2" rx="1" fill="white" />
81 </svg>
82 </span>
83 ) : (
84 <svg width="13" height="10" viewBox="0 0 13 10" fill="none">
85 <path
86 d="M1.5 5l3.5 3.5L11.5 1"
87 stroke="white"
88 strokeWidth="2"
89 strokeLinecap="round"
90 strokeLinejoin="round"
91 className="animate-draw-check"
92 />
93 </svg>
94 )
95 )}
96 </button>
97 </div>
98 );
99}