@@ -13,6 +13,7 @@ import {
1313 Popover ,
1414 PopoverContent ,
1515 PopoverTrigger ,
16+ AnimatedCount ,
1617} from "@betterinternship/components" ;
1718import { Calendar } from "@/components/ui/calendar" ;
1819import {
@@ -25,6 +26,7 @@ import * as RadioGroup from "@radix-ui/react-radio-group";
2526import {
2627 CalendarDays ,
2728 Check ,
29+ CheckCheck ,
2830 ChevronDown ,
2931 ChevronLeft ,
3032 ChevronRight ,
@@ -33,14 +35,14 @@ import {
3335 Minus ,
3436} from "lucide-react" ;
3537import * as React from "react" ;
36- import { createContext , useContext , useRef } from "react" ;
38+ import { createContext , useContext , useEffect , useRef } from "react" ;
3739import "react-datepicker/dist/react-datepicker.css" ;
3840import { GroupableRadioDropdown } from "./ui/dropdown" ;
3941import { Input } from "@betterinternship/components" ;
4042import { Tooltip } from "react-tooltip" ;
4143import { Textarea } from "./ui/textarea" ;
4244import { Matcher } from "react-day-picker" ;
43- import { AnimatePresence , motion } from "framer-motion" ;
45+ import { AnimatePresence , motion , useReducedMotion } from "framer-motion" ;
4446import { useBlurTransition } from "./animata/blur" ;
4547
4648interface EditFormContext < T extends IFormData > {
@@ -439,14 +441,26 @@ export const FormCheckbox = ({
439441interface FormCheckBoxGroupProps extends React . InputHTMLAttributes < HTMLInputElement > {
440442 options : { value : string | number ; label : string ; description ?: string } [ ] ;
441443 values : ( string | number ) [ ] ;
442- setter : ( value : any ) => void ;
444+ setter : ( values : ( string | number ) [ ] ) => void ;
443445 label ?: string ;
444446 required ?: boolean ;
445447 className ?: string ;
446448 tooltip ?: string ;
447449 tooltipId ?: string ;
450+ /** Multi-select hint shown above the options. Pass `null` to hide it. */
451+ hint ?: React . ReactNode ;
452+ /** Columns on md+ screens. Defaults to 3. */
453+ columns ?: 1 | 2 | 3 ;
454+ /** Fired when a single option is toggled, before the setter runs. */
455+ onToggle ?: ( value : string | number , checked : boolean ) => void ;
448456}
449457
458+ const GRID_COLS : Record < 1 | 2 | 3 , string > = {
459+ 1 : "" ,
460+ 2 : "md:grid-cols-2" ,
461+ 3 : "md:grid-cols-3" ,
462+ } ;
463+
450464export const FormCheckBoxGroup = ( {
451465 options,
452466 values,
@@ -456,16 +470,23 @@ export const FormCheckBoxGroup = ({
456470 className,
457471 tooltip,
458472 tooltipId,
473+ hint = "Select all that apply" ,
474+ columns = 3 ,
475+ onToggle,
459476 ...props
460477} : FormCheckBoxGroupProps ) => {
461478 const handleValueChange = ( optionValue : string | number ) => {
462- if ( values . includes ( optionValue ) ) {
463- setter ( values . filter ( ( v ) => v !== optionValue ) ) ;
464- } else {
465- setter ( [ ...values , optionValue ] ) ;
466- }
479+ const willBeChecked = ! values . includes ( optionValue ) ;
480+ onToggle ?.( optionValue , willBeChecked ) ;
481+ setter (
482+ willBeChecked
483+ ? [ ...values , optionValue ]
484+ : values . filter ( ( v ) => v !== optionValue ) ,
485+ ) ;
467486 } ;
468487
488+ const blurTransition = useBlurTransition ( ) ;
489+
469490 return (
470491 < div className = { cn ( "space-y-3" , className ) } >
471492 { label && (
@@ -477,16 +498,43 @@ export const FormCheckBoxGroup = ({
477498 />
478499 ) }
479500
480- < div className = "grid grid-cols-1 md:grid-cols-3 gap-4" >
501+ { ( hint || values . length > 0 ) && (
502+ < div className = "flex items-center gap-2 text-xs" >
503+ { hint && (
504+ < span className = "inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 font-medium text-muted-foreground" >
505+ < CheckCheck className = "h-3 w-3" />
506+ { hint }
507+ </ span >
508+ ) }
509+ < AnimatePresence >
510+ { values . length > 0 && (
511+ < motion . span
512+ className = "font-medium text-primary"
513+ { ...blurTransition }
514+ >
515+ < AnimatedCount value = { values . length } /> selected
516+ </ motion . span >
517+ ) }
518+ </ AnimatePresence >
519+ </ div >
520+ ) }
521+
522+ < div className = { cn ( "grid grid-cols-1 gap-4" , GRID_COLS [ columns ] ) } >
481523 { options . map ( ( option ) => {
482524 const isChecked = values . includes ( option . value ) ;
483525
484526 return (
485527 < div
486528 key = { option . value }
529+ role = "checkbox"
530+ aria-checked = { isChecked }
487531 onClick = { ( ) => handleValueChange ( option . value ) }
488- className = { `flex items-start gap-4 p-3 border rounded-[0.33em] transition-colors cursor-pointer h-fit
489- ${ isChecked ? "border-primary border-opacity-85" : "border-gray-200 hover:border-gray-300" } ` }
532+ className = { cn (
533+ "flex items-start gap-3 p-3 border rounded-[0.33em] transition-colors cursor-pointer h-fit" ,
534+ isChecked
535+ ? "border-primary border-opacity-85 bg-primary/5"
536+ : "border-gray-200 hover:border-gray-300 hover:bg-gray-50" ,
537+ ) }
490538 >
491539 < FormCheckbox checked = { isChecked ?? false } />
492540 < div className = "grid grid-rows-1 md:grid-rows-2" >
0 commit comments