{ "name": "team-selector", "type": "registry:component", "dependencies": [ "motion", "lucide-react" ], "files": [ { "type": "registry:component", "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Team Selector\n * @version: 3.0.0\n * @date: 2026-04-23\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { Minus, Plus } from \"lucide-react\";\nimport {\n AnimatePresence,\n motion,\n useReducedMotion,\n type Variants,\n} from \"motion/react\";\nimport Image from \"next/image\";\nimport { useRef, useState } from \"react\";\n\nconst AVATAR_OVERLAP = 12;\nconst DICEBEAR_STYLE = \"notionists-neutral\";\nconst dicebearUrl = (seed: string) =>\n `https://api.dicebear.com/9.x/${DICEBEAR_STYLE}/svg?seed=${encodeURIComponent(seed)}&backgroundColor=f4f4f5,e4e4e7,d4d4d8,a1a1aa`;\n\ninterface TeamMember {\n id: string;\n name: string;\n avatarUrl: string;\n}\n\nconst DEFAULT_MEMBERS: TeamMember[] = [\n { id: \"member-1\", name: \"Alex Rivera\", avatarUrl: dicebearUrl(\"Alex\") },\n { id: \"member-2\", name: \"Blair Kim\", avatarUrl: dicebearUrl(\"Blair\") },\n { id: \"member-3\", name: \"Casey Lin\", avatarUrl: dicebearUrl(\"Casey\") },\n { id: \"member-4\", name: \"Devon Marsh\", avatarUrl: dicebearUrl(\"Devon\") },\n];\n\nconst animations = {\n avatar: {\n visible: {\n opacity: 1,\n scale: 1,\n transition: {\n type: \"spring\",\n stiffness: 260,\n damping: 22,\n mass: 0.6,\n },\n },\n hidden: {\n opacity: 0,\n scale: 0.85,\n transition: { duration: 0.18, ease: \"easeOut\" },\n },\n } satisfies Variants,\n vibration: {\n idle: { x: 0 },\n shake: {\n x: [-3, 3, -2, 2, 0] as const,\n transition: { duration: 0.28, ease: \"easeOut\" },\n },\n } satisfies Variants,\n} as const;\n\ninterface TeamSelectorProps {\n members?: TeamMember[];\n defaultValue?: number;\n onChange?: (size: number) => void;\n label?: string;\n className?: string;\n}\n\nexport default function TeamSelector({\n members = DEFAULT_MEMBERS,\n defaultValue = 1,\n onChange,\n label = \"Team Size\",\n className = \"\",\n}: TeamSelectorProps) {\n const maxTeamSize = members.length;\n const [peopleCount, setPeopleCount] = useState(defaultValue);\n const [isVibrating, setIsVibrating] = useState(false);\n const directionRef = useRef<1 | -1>(1);\n const prefersReducedMotion = useReducedMotion();\n\n const triggerVibration = () => {\n if (prefersReducedMotion) {\n return;\n }\n setIsVibrating(true);\n setTimeout(() => setIsVibrating(false), 280);\n };\n\n const handleIncrement = (e: React.MouseEvent | React.KeyboardEvent) => {\n e.preventDefault();\n if (peopleCount < maxTeamSize) {\n directionRef.current = 1;\n const newCount = peopleCount + 1;\n setPeopleCount(newCount);\n onChange?.(newCount);\n } else {\n triggerVibration();\n }\n };\n\n const handleDecrement = (e: React.MouseEvent | React.KeyboardEvent) => {\n e.preventDefault();\n if (peopleCount > 1) {\n directionRef.current = -1;\n const newCount = peopleCount - 1;\n setPeopleCount(newCount);\n onChange?.(newCount);\n } else {\n triggerVibration();\n }\n };\n\n const handleKeyDown = (\n e: React.KeyboardEvent,\n action: \"increment\" | \"decrement\"\n ) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (action === \"increment\") {\n handleIncrement(e);\n } else {\n handleDecrement(e);\n }\n }\n };\n\n const counterDistance = prefersReducedMotion ? 0 : 10;\n\n return (\n