{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "team-selector",
  "type": "registry:component",
  "title": "Team Selector",
  "description": "Stepper that reveals an overlapping avatar stack as the count grows (KokonutUI, MIT), bridged to contract tokens.",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "registry/components/team-selector.tsx",
      "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 * Bridged to the Subconscious design contract: color → theme tokens, redundant\n * dark: variants dropped, next/image → plain <img>. Motion preserved. The two\n * lucide-react imports (Minus, Plus) were inlined as raw SVG per issue #463;\n * stroke style follows the brand iconography canon (square cap, miter join)\n * rather than lucide's round/round default. Icon path data from\n * lucide-react@0.400.0 (ISC).\n */\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"framer-motion\";\nimport { useRef, useState } from \"react\";\n\ntype IconProps = React.SVGProps<SVGSVGElement>;\n\nfunction MinusIcon({ className, strokeWidth = 1.5, ...props }: IconProps) {\n  return (\n    <svg\n      className={className}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"square\"\n      strokeLinejoin=\"miter\"\n      strokeWidth={strokeWidth}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path d=\"M5 12h14\" />\n    </svg>\n  );\n}\n\nfunction PlusIcon({ className, strokeWidth = 1.5, ...props }: IconProps) {\n  return (\n    <svg\n      className={className}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"square\"\n      strokeLinejoin=\"miter\"\n      strokeWidth={strokeWidth}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path d=\"M5 12h14\" />\n      <path d=\"M12 5v14\" />\n    </svg>\n  );\n}\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  const stepButton =\n    \"flex size-9 items-center justify-center rounded-md border transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-40\";\n  const stepButtonStyle: React.CSSProperties = {\n    background: \"var(--backgroundColor)\",\n    color: \"color-mix(in srgb, var(--color) 60%, transparent)\",\n    borderColor: \"color-mix(in srgb, var(--color) 15%, transparent)\",\n  };\n\n  return (\n    <div\n      className={`flex w-full flex-col items-center justify-center ${className}`}\n    >\n      <div\n        className=\"w-full max-w-xs rounded-md border p-5\"\n        style={{\n          background: \"var(--backgroundColor)\",\n          borderColor: \"color-mix(in srgb, var(--color) 15%, transparent)\",\n        }}\n      >\n        <fieldset>\n          <legend\n            className=\"mb-5 w-full font-medium text-[11px] uppercase tracking-[0.14em]\"\n            style={{\n              color: \"color-mix(in srgb, var(--color) 60%, transparent)\",\n            }}\n          >\n            {label}\n          </legend>\n\n          <div className=\"mb-7 flex justify-center\">\n            <div className=\"flex items-center\">\n              {members.map((member, index) => (\n                <motion.div\n                  animate={index < peopleCount ? \"visible\" : \"hidden\"}\n                  className=\"flex items-center justify-center\"\n                  initial={index < defaultValue ? \"visible\" : \"hidden\"}\n                  key={member.id}\n                  style={{\n                    marginLeft: index === 0 ? 0 : -AVATAR_OVERLAP,\n                    zIndex: maxTeamSize - index,\n                  }}\n                  variants={animations.avatar}\n                >\n                  {/* biome-ignore lint/performance/noImgElement: registry component is framework-agnostic */}\n                  <img\n                    alt={member.name}\n                    className=\"size-11 rounded-full border-2 object-cover ring-1\"\n                    src={member.avatarUrl}\n                    style={{\n                      borderColor: \"var(--backgroundColor)\",\n                      background:\n                        \"color-mix(in srgb, var(--color) 8%, transparent)\",\n                    }}\n                  />\n                </motion.div>\n              ))}\n            </div>\n          </div>\n\n          <motion.div\n            animate={isVibrating ? \"shake\" : \"idle\"}\n            className=\"flex items-center justify-center gap-5\"\n            initial=\"idle\"\n            variants={animations.vibration}\n          >\n            <button\n              aria-label=\"Decrease team size\"\n              className={stepButton}\n              disabled={peopleCount <= 1}\n              onClick={handleDecrement}\n              onKeyDown={(e) => handleKeyDown(e, \"decrement\")}\n              style={stepButtonStyle}\n              type=\"button\"\n            >\n              <MinusIcon aria-hidden=\"true\" className=\"size-3.5\" strokeWidth={2} />\n            </button>\n\n            <div className=\"flex min-w-16 flex-col items-center\">\n              <div className=\"relative h-9 overflow-hidden\">\n                <AnimatePresence initial={false} mode=\"popLayout\">\n                  <motion.output\n                    animate={{ opacity: 1, y: 0 }}\n                    aria-live=\"polite\"\n                    className=\"block select-none font-semibold text-3xl tabular-nums\"\n                    style={{ color: \"var(--color)\" }}\n                    exit={{\n                      opacity: 0,\n                      y: directionRef.current * -counterDistance,\n                      transition: { duration: 0.14, ease: \"easeIn\" },\n                    }}\n                    initial={{\n                      opacity: 0,\n                      y: directionRef.current * counterDistance,\n                    }}\n                    key={peopleCount}\n                    transition={{ duration: 0.2, ease: \"easeOut\" }}\n                  >\n                    {peopleCount}\n                  </motion.output>\n                </AnimatePresence>\n              </div>\n              <span\n                className=\"text-[11px]\"\n                style={{\n                  color: \"color-mix(in srgb, var(--color) 60%, transparent)\",\n                }}\n              >\n                {peopleCount === 1 ? \"member\" : \"members\"}\n              </span>\n            </div>\n\n            <button\n              aria-label=\"Increase team size\"\n              className={stepButton}\n              disabled={peopleCount >= maxTeamSize}\n              onClick={handleIncrement}\n              onKeyDown={(e) => handleKeyDown(e, \"increment\")}\n              style={stepButtonStyle}\n              type=\"button\"\n            >\n              <PlusIcon aria-hidden=\"true\" className=\"size-3.5\" strokeWidth={2} />\n            </button>\n          </motion.div>\n        </fieldset>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}
