{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select-multi",
  "title": "Multi select",
  "description": "Styled select: the Multi set. 5 decorative select variations (TagsSelect, ChipsSelect, CountSelect, ChecksSelect, InlineMultiSelect) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/select-multi.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronDown, X } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Multi select family: 5 self-contained, GENUINELY multi-select listboxes. The only\n   difference is how the selected values are summarized in the trigger and how the\n   option row looks (tag, chip, counter, checkbox, inline list). In multi-select,\n   clicking an option TOGGLES the value and the panel stays open (so selection can\n   continue); it closes on an outside click or Escape. The selection state lives in\n   the ROOT component, so it does not disappear even when the panel unmounts. NO\n   radix, NO portal. Color comes ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst triggerHeight: Record<StyledSize, string> = {\n  sm: \"min-h-8 text-sm\",\n  md: \"min-h-9 text-sm\",\n  lg: \"min-h-10 text-base\",\n  xl: \"min-h-12 text-base\",\n}\n\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-48\",\n  md: \"w-56\",\n  lg: \"w-64\",\n  xl: \"w-72\",\n}\n\nexport interface StyledSelectOption {\n  value: string\n  label: string\n}\n\nexport interface StyledMultiSelectProps {\n  className?: string\n  size?: StyledSize\n  placeholder?: string\n  options?: StyledSelectOption[]\n  /** Multi-selection: the controlled value array. */\n  value?: string[]\n  /** Cok secim: uncontrolled baslangic dizisi. */\n  defaultValue?: string[]\n  onValueChange?: (v: string[]) => void\n}\n\nconst defaultOptions: StyledSelectOption[] = [\n  { value: \"design\", label: \"Design\" },\n  { value: \"engineering\", label: \"Engineering\" },\n  { value: \"marketing\", label: \"Marketing\" },\n  { value: \"support\", label: \"Support\" },\n  { value: \"sales\", label: \"Sales\" },\n]\n\nconst triggerBase =\n  \"inline-flex w-full shrink-0 select-none items-center justify-between gap-2 rounded-lg border border-field-border bg-background px-3 py-1 text-left font-medium text-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst panelBase =\n  \"absolute left-0 top-full z-50 mt-1 max-h-72 min-w-48 origin-top overflow-auto rounded-xl border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none\"\n\nconst optionBase =\n  \"flex w-full cursor-default select-none items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst panelMotion = {\n  initial: { opacity: 0, scale: 0.96, y: -6 },\n  animate: { opacity: 1, scale: 1, y: 0 },\n  exit: { opacity: 0, scale: 0.96, y: -6 },\n  transition: { type: \"spring\" as const, stiffness: 340, damping: 26 },\n}\n\nconst tagBase =\n  \"inline-flex max-w-full items-center gap-1 rounded-md border border-[color-mix(in_oklab,var(--color-border)_80%,transparent)] bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground\"\n\ntype Summary = \"tags\" | \"chips\" | \"count\" | \"inline\"\n\ninterface MultiFlags {\n  /** Trigger'daki secim ozeti bicimi. */\n  summary: Summary\n  /** Shows a square checkbox on the option row. */\n  checkbox?: boolean\n  /** tags/chips ozetinde en fazla kac rozet gosterilir. */\n  maxTags?: number\n}\n\nfunction BaseMultiSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n  summary,\n  checkbox,\n  maxTags = 2,\n}: StyledMultiSelectProps & MultiFlags) {\n  const list = options ?? defaultOptions\n  const reduce = useReducedMotion()\n  const uid = React.useId()\n  const listId = `${uid}-listbox`\n  const optionId = (i: number) => `${uid}-option-${i}`\n\n  const [open, setOpen] = React.useState(false)\n  const [active, setActive] = React.useState(0)\n  const [internal, setInternal] = React.useState<string[]>(defaultValue ?? [])\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n\n  const isControlled = value !== undefined\n  const selected = isControlled ? value : internal\n  const selectedOptions = list.filter((o) => selected.includes(o.value))\n\n  const commit = React.useCallback(\n    (next: string[]) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n    },\n    [isControlled, onValueChange],\n  )\n\n  const toggle = React.useCallback(\n    (v: string) => {\n      commit(selected.includes(v) ? selected.filter((x) => x !== v) : [...selected, v])\n    },\n    [commit, selected],\n  )\n\n  const close = React.useCallback(() => {\n    setOpen(false)\n    triggerRef.current?.focus()\n  }, [])\n\n  React.useEffect(() => {\n    if (!open) return\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) setOpen(false)\n    }\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => window.removeEventListener(\"pointerdown\", onPointer)\n  }, [open])\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Escape\") {\n      if (!open) return\n      e.preventDefault()\n      close()\n      return\n    }\n    if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault()\n      if (!open) {\n        setOpen(true)\n        setActive(0)\n        return\n      }\n      const dir = e.key === \"ArrowDown\" ? 1 : -1\n      setActive((i) => (i + dir + list.length) % list.length)\n      return\n    }\n    if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault()\n      if (!open) {\n        setOpen(true)\n        setActive(0)\n        return\n      }\n      const opt = list[active]\n      if (opt) toggle(opt.value)\n    }\n  }\n\n  const fade = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n  const anim = reduce ? fade : panelMotion\n\n  let triggerContent: React.ReactNode\n  if (selectedOptions.length === 0) {\n    triggerContent = <span className=\"truncate text-muted-foreground\">{placeholder}</span>\n  } else if (summary === \"count\") {\n    triggerContent = (\n      <span className=\"flex min-w-0 items-center gap-2\">\n        <span className=\"inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground\">\n          {selectedOptions.length}\n        </span>\n        <span className=\"truncate\">selected</span>\n      </span>\n    )\n  } else if (summary === \"inline\") {\n    triggerContent = <span className=\"truncate\">{selectedOptions.map((o) => o.label).join(\", \")}</span>\n  } else {\n    const shown = selectedOptions.slice(0, maxTags)\n    const rest = selectedOptions.length - shown.length\n    triggerContent = (\n      <span className=\"flex min-w-0 flex-wrap items-center gap-1 py-1\">\n        {shown.map((o) => (\n          <span key={o.value} className={cn(tagBase, summary === \"chips\" && \"rounded-full px-2\")}>\n            <span className=\"truncate\">{o.label}</span>\n            {summary === \"tags\" ? (\n              <span\n                aria-hidden=\"true\"\n                className=\"inline-flex shrink-0 cursor-default items-center rounded-sm text-muted-foreground transition-colors hover:text-foreground [&_svg]:size-3 [&_svg]:shrink-0 [&_i]:text-xs [&_i]:leading-none\"\n                onPointerDown={(e) => e.stopPropagation()}\n                onClick={(e) => {\n                  e.stopPropagation()\n                  toggle(o.value)\n                }}\n              >\n                <X />\n              </span>\n            ) : null}\n          </span>\n        ))}\n        {rest > 0 ? (\n          <span className={cn(tagBase, summary === \"chips\" && \"rounded-full px-2\", \"text-muted-foreground\")}>\n            +{rest}\n          </span>\n        ) : null}\n      </span>\n    )\n  }\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"styled-select\"\n      className={cn(\"relative inline-flex w-56 max-w-full\", panelWidth[size], className)}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        data-slot=\"styled-select-trigger\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        aria-controls={listId}\n        aria-activedescendant={open ? optionId(active) : undefined}\n        className={cn(triggerBase, triggerHeight[size])}\n        onClick={() => setOpen(!open)}\n        onKeyDown={onKeyDown}\n      >\n        {triggerContent}\n        <ChevronDown\n          className={cn(\"shrink-0 opacity-70 transition-transform duration-200\", open && \"rotate-180\")}\n        />\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            id={listId}\n            role=\"listbox\"\n            aria-multiselectable\n            data-slot=\"styled-select-content\"\n            className={cn(panelBase, panelWidth[size])}\n            initial={anim.initial}\n            animate={anim.animate}\n            exit={anim.exit}\n            transition={reduce ? { duration: 0.14 } : panelMotion.transition}\n          >\n            <div className=\"flex flex-col\">\n              {list.map((option, i) => {\n                const isSelected = selected.includes(option.value)\n                return (\n                  <button\n                    key={option.value}\n                    id={optionId(i)}\n                    type=\"button\"\n                    role=\"option\"\n                    tabIndex={-1}\n                    aria-selected={isSelected}\n                    className={cn(optionBase, i === active && \"bg-accent text-accent-foreground\")}\n                    onPointerEnter={() => setActive(i)}\n                    onClick={() => toggle(option.value)}\n                  >\n                    {checkbox ? (\n                      <span\n                        aria-hidden=\"true\"\n                        className={cn(\n                          \"flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-field-border transition-colors\",\n                          isSelected && \"border-primary bg-primary text-primary-foreground\",\n                        )}\n                      >\n                        {isSelected ? <Check className=\"size-3\" /> : null}\n                      </span>\n                    ) : (\n                      <span className=\"flex size-4 shrink-0 items-center justify-center text-primary\">\n                        {isSelected ? <Check /> : null}\n                      </span>\n                    )}\n                    <span className=\"truncate\">{option.label}</span>\n                  </button>\n                )\n              })}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* Tags: the selected items sit in the trigger as removable tags. */\nexport function TagsSelect(props: StyledMultiSelectProps) {\n  return <BaseMultiSelect {...props} summary=\"tags\" />\n}\n\n/* Chips: secilenler yuvarlak chip'ler, tasanlar +N ile ozetlenir. */\nexport function ChipsSelect(props: StyledMultiSelectProps) {\n  return <BaseMultiSelect {...props} summary=\"chips\" maxTags={2} />\n}\n\n/* Count: the trigger shows only the number of selections in a badge. */\nexport function CountSelect(props: StyledMultiSelectProps) {\n  return <BaseMultiSelect {...props} summary=\"count\" />\n}\n\n/* Checks: the option rows carry a square checkbox. */\nexport function ChecksSelect(props: StyledMultiSelectProps) {\n  return <BaseMultiSelect {...props} summary=\"count\" checkbox />\n}\n\n/* InlineMulti: the selected items read as one comma-separated line in the trigger. */\nexport function InlineMultiSelect(props: StyledMultiSelectProps) {\n  return <BaseMultiSelect {...props} summary=\"inline\" checkbox />\n}\n",
      "type": "registry:component",
      "target": "components/ui/select-multi.tsx"
    }
  ],
  "categories": [
    "styled",
    "select"
  ],
  "type": "registry:component"
}