{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "title": "MultiSelect",
  "description": "Pick several. Picks become chips; Backspace removes the last one.",
  "registryDependencies": [
    "https://creance.pages.dev/r/combobox.json",
    "https://creance.pages.dev/r/creance-base.json",
    "https://creance.pages.dev/r/input.json"
  ],
  "files": [
    {
      "path": "multi-select/MultiSelect.tsx",
      "content": "'use client';\n\nimport { useRef, useState } from 'react';\nimport { cx } from '@/lib/creance/cx';\nimport { CheckIcon, CrossIcon, PlusIcon } from '@/lib/creance/icons';\nimport { Highlight, useItems, type ComboboxBaseProps, type Item } from '@/components/creance/combobox/Combobox';\nimport { useListbox } from '@/components/creance/combobox/useListbox';\nimport '@/components/creance/input/Input.css';\nimport '@/components/creance/combobox/Combobox.css';\n\nexport interface MultiSelectProps extends ComboboxBaseProps {\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  /** Accessible name prefix of each chip's remove button. Default \"Remove\". */\n  removeLabel?: string;\n}\n\n/**\n * Pick several from a list. Picks become chips; Enter toggles the highlighted option and the\n * list stays open; Backspace in an empty field removes the last chip.\n */\nexport function MultiSelect(props: MultiSelectProps) {\n  const { value, defaultValue = [], onValueChange, placeholder, emptyMessage = 'No matches', onCreate, createLabel, disabled, id, className, removeLabel = 'Remove' } = props;\n  const [own, setOwn] = useState<string[]>(defaultValue);\n  const selected = value ?? own;\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState('');\n  const input = useRef<HTMLInputElement>(null);\n  const byValue = new Map(props.options.map((o) => [o.value, o]));\n\n  const set = (next: string[]) => {\n    if (value === undefined) setOwn(next);\n    onValueChange?.(next);\n  };\n  const items = useItems(props, query, true);\n  const pick = (item: Item) => {\n    if (item.create !== undefined) {\n      onCreate?.(item.create);\n    } else {\n      const v = item.option!.value;\n      set(selected.includes(v) ? selected.filter((s) => s !== v) : [...selected, v]);\n    }\n    setQuery('');\n  };\n  const list = useListbox(items, open, pick);\n\n  return (\n    <div\n      className={cx('cr-combobox', className)}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget)) {\n          setOpen(false);\n          setQuery('');\n        }\n      }}\n    >\n      {/* A mouse convenience: clicking between chips focuses the input, which is already in the tab order. */}\n      {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}\n      <div\n        className=\"cr-combobox__field cr-input\"\n        data-multiple=\"\"\n        data-disabled={disabled || undefined}\n        aria-invalid={props['aria-invalid']}\n        onClick={() => input.current?.focus()}\n      >\n        {selected.map((v) => {\n          const label = byValue.get(v)?.label ?? v;\n          return (\n            <span key={v} className=\"cr-combobox__chip\">\n              {label}\n              <button\n                type=\"button\"\n                className=\"cr-combobox__chip-remove\"\n                aria-label={`${removeLabel} ${label}`}\n                disabled={disabled}\n                onClick={(e) => {\n                  e.stopPropagation();\n                  set(selected.filter((s) => s !== v));\n                  input.current?.focus();\n                }}\n              >\n                <CrossIcon />\n              </button>\n            </span>\n          );\n        })}\n        <input\n          ref={input}\n          id={id}\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-controls={list.listId}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={list.activeDescendant}\n          aria-label={props['aria-label']}\n          aria-labelledby={props['aria-labelledby']}\n          aria-describedby={props['aria-describedby']}\n          aria-invalid={props['aria-invalid']}\n          className=\"cr-combobox__input\"\n          autoComplete=\"off\"\n          placeholder={placeholder}\n          disabled={disabled}\n          value={query}\n          onChange={(e) => {\n            setQuery(e.currentTarget.value);\n            setOpen(true);\n          }}\n          onFocus={() => setOpen(true)}\n          onKeyDown={(e) => {\n            if (list.onKeyDown(e, () => setOpen(true))) return;\n            if (e.key === 'Backspace' && !query && selected.length) set(selected.slice(0, -1));\n            if (e.key === 'Escape' && open) {\n              e.preventDefault();\n              setOpen(false);\n            }\n          }}\n        />\n      </div>\n      <ul\n        id={list.listId}\n        role=\"listbox\"\n        aria-multiselectable=\"true\"\n        aria-label={props['aria-label']}\n        aria-labelledby={props['aria-labelledby']}\n        className=\"cr-combobox__list cr-popover\"\n        hidden={!open}\n        onMouseDown={(e) => e.preventDefault()}\n      >\n        {items.map((item, i) => (\n          // Options are reached from the keyboard through the input (aria-activedescendant).\n          // eslint-disable-next-line jsx-a11y/click-events-have-key-events\n          <li\n            key={item.key}\n            id={list.optionId(i)}\n            role=\"option\"\n            aria-selected={item.option ? selected.includes(item.option.value) : false}\n            aria-disabled={item.disabled || undefined}\n            className=\"cr-combobox__option\"\n            data-active={i === list.active || undefined}\n            data-create={item.create !== undefined || undefined}\n            onMouseMove={() => !item.disabled && list.setActive(i)}\n            onClick={() => !item.disabled && pick(item)}\n          >\n            {item.option ? (\n              <>\n                <span className=\"cr-combobox__check\">{selected.includes(item.option.value) && <CheckIcon />}</span>\n                {item.option.leading}\n                <Highlight label={item.option.label} query={query} />\n                {item.option.meta && <span className=\"cr-combobox__meta\">{item.option.meta}</span>}\n              </>\n            ) : (\n              <>\n                <PlusIcon />\n                {createLabel ? createLabel(item.create!) : `Create “${item.create}”`}\n              </>\n            )}\n          </li>\n        ))}\n        {items.length === 0 && (\n          <li role=\"presentation\" className=\"cr-combobox__empty\">\n            {emptyMessage}\n          </li>\n        )}\n      </ul>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/multi-select/MultiSelect.tsx"
    }
  ],
  "categories": [
    "molecule",
    "forms"
  ],
  "type": "registry:ui"
}