Visually highlights selected items by sliding a background into view when hovered over or clicked.
1'use client';
2import { cn } from '@/lib/utils';
3import { AnimatePresence, Transition, motion } from 'motion/react';
4import {
5 Children,
6 cloneElement,
7 ReactElement,
8 useEffect,
9 useState,
10 useId,
11} from 'react';
12
13export type AnimatedBackgroundProps = {
14 children:
15 | ReactElement<{ 'data-id': string }>[]
16 | ReactElement<{ 'data-id': string }>;
17 defaultValue?: string;
18 onValueChange?: (newActiveId: string | null) => void;
19 className?: string;
20 transition?: Transition;
21 enableHover?: boolean;
22};
23
24export function AnimatedBackground({
25 children,
26 defaultValue,
27 onValueChange,
28 className,
29 transition,
30 enableHover = false,
31}: AnimatedBackgroundProps) {
32 const [activeId, setActiveId] = useState<string | null>(null);
33 const uniqueId = useId();
34
35 const handleSetActiveId = (id: string | null) => {
36 setActiveId(id);
37
38 if (onValueChange) {
39 onValueChange(id);
40 }
41 };
42
43 useEffect(() => {
44 if (defaultValue !== undefined) {
45 setActiveId(defaultValue);
46 }
47 }, [defaultValue]);
48
49 return Children.map(children, (child: any, index) => {
50 const id = child.props['data-id'];
51
52 const interactionProps = enableHover
53 ? {
54 onMouseEnter: () => handleSetActiveId(id),
55 onMouseLeave: () => handleSetActiveId(null),
56 }
57 : {
58 onClick: () => handleSetActiveId(id),
59 };
60
61 return cloneElement(
62 child,
63 {
64 key: index,
65 className: cn('relative inline-flex', child.props.className),
66 'data-checked': activeId === id ? 'true' : 'false',
67 ...interactionProps,
68 },
69 <>
70 <AnimatePresence initial={false}>
71 {activeId === id && (
72 <motion.div
73 layoutId={`background-${uniqueId}`}
74 className={cn('absolute inset-0', className)}
75 transition={transition}
76 initial={{ opacity: defaultValue ? 1 : 0 }}
77 animate={{
78 opacity: 1,
79 }}
80 exit={{
81 opacity: 0,
82 }}
83 />
84 )}
85 </AnimatePresence>
86 <div className='z-10'>{child.props.children}</div>
87 </>
88 );
89 });
90}
91