{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "multi-select",
  "type": "registry:ui",
  "title": "MultiSelect",
  "description": "انتخاب چندتایی",
  "dependencies": [
    "@kashi-ui/persian@^1.8.0"
  ],
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/badge.json",
    "https://kashi.sepantapouya.com/r/combobox.json",
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/field.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/MultiSelect.tsx",
      "type": "registry:ui",
      "target": "components/kashi/MultiSelect.tsx",
      "content": "\"use client\";\n\nimport { useId, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { toFaDigits } from \"@kashi-ui/persian\";\nimport { Badge } from \"./Badge\";\nimport { defaultFilter, highlight, type ComboboxOption } from \"./Combobox\";\nimport { Field, describedBy } from \"./Field\";\nimport { cx } from \"./cx\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport interface MultiSelectProps {\n  label?: ReactNode;\n  hint?: ReactNode;\n  error?: ReactNode;\n  options: ComboboxOption[];\n  value?: string[];\n  defaultValue?: string[];\n  onChange?: (value: string[]) => void;\n  placeholder?: string;\n  /** بیشترین تعداد انتخاب */\n  max?: number;\n  filter?: (option: ComboboxOption, query: string) => boolean;\n  /** @default \"نتیجه‌ای پیدا نشد.\" */\n  emptyMessage?: ReactNode;\n  disabled?: boolean;\n  /** با name، یک فیلد پنهان برای هر مقدار */\n  name?: string;\n  id?: string;\n  className?: string;\n  labels?: { remove?: (label: string) => string; selected?: (n: number) => string; max?: (n: number) => string; clear?: string };\n}\n\nconst Check = () => <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m5 10.5 3.2 3L15 7\" /></svg>;\n\n/**\n * انتخاب چند گزینه با جستجو؛ انتخاب‌ها به‌شکل برچسب داخل فیلد.\n * ↑↓ حرکت، Enter انتخاب یا لغو انتخاب، Backspace در فیلد خالی آخرین برچسب را حذف می‌کند، Esc بستن.\n */\nexport function MultiSelect({\n  label, hint, error, options, value, defaultValue = [], onChange, placeholder, max, filter = defaultFilter,\n  emptyMessage, disabled, name, id, className, labels = {},\n}: MultiSelectProps) {\n  const { messages: M, n: N } = useKashi();\n  emptyMessage ??= M.common.noResults;\n  const {\n    remove = M.common.remove, selected: selectedLabel = (x: number) => M.multiSelect.selected(N(x)),\n    max: maxLabel = (x: number) => M.common.max(N(x)), clear = M.common.clearAll,\n  } = labels;\n  const autoId = useId();\n  const inputId = id ?? autoId;\n  const listId = `${inputId}-list`;\n  const [inner, setInner] = useState(defaultValue);\n  const chosen = value ?? inner;\n  const [query, setQuery] = useState(\"\");\n  const [open, setOpen] = useState(false);\n  const [active, setActive] = useState(-1);\n  const [announce, setAnnounce] = useState(\"\");\n  const input = useRef<HTMLInputElement>(null);\n  const wrap = useRef<HTMLDivElement>(null);\n  const filtered = useMemo(() => options.filter((o) => filter(o, query)), [options, query, filter]);\n  const full = max !== undefined && chosen.length >= max;\n\n  const set = (next: string[]) => {\n    if (value === undefined) setInner(next);\n    onChange?.(next);\n  };\n  const toggle = (o: ComboboxOption) => {\n    if (o.disabled) return;\n    const on = chosen.includes(o.value);\n    if (!on && full) return setAnnounce(maxLabel(max!));\n    set(on ? chosen.filter((v) => v !== o.value) : [...chosen, o.value]);\n    setAnnounce(on ? M.common.removed(o.label) : M.common.selected(o.label));\n    setQuery(\"\");\n  };\n  const move = (d: number) => {\n    const en = filtered.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0);\n    if (!en.length) return;\n    const p = en.indexOf(active);\n    setActive(en[(p + d + en.length) % en.length]);\n  };\n  const onKey = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"ArrowDown\") { e.preventDefault(); open ? move(1) : (setOpen(true), setActive(0)); }\n    else if (e.key === \"ArrowUp\") { e.preventDefault(); open ? move(-1) : setOpen(true); }\n    else if (e.key === \"Enter\" && open && filtered[active]) { e.preventDefault(); toggle(filtered[active]); }\n    else if (e.key === \"Escape\" && open) { e.preventDefault(); setOpen(false); }\n    else if (e.key === \"Backspace\" && !query && chosen.length) {\n      const last = options.find((o) => o.value === chosen[chosen.length - 1]);\n      set(chosen.slice(0, -1));\n      if (last) setAnnounce(M.common.removed(last.label));\n    }\n  };\n  const labelOf = (v: string) => options.find((o) => o.value === v)?.label ?? v;\n\n  return (\n    <Field id={inputId} label={label} hint={hint} error={error} className={className}>\n      <div\n        ref={wrap}\n        className=\"k-combobox k-multiselect\"\n        data-open={open ? \"\" : undefined}\n        onBlur={(e) => !wrap.current?.contains(e.relatedTarget as Node) && setOpen(false)}\n      >\n        <div className=\"k-tag-input\" data-invalid={error ? \"\" : undefined} onClick={() => input.current?.focus()}>\n          {chosen.map((v) => (\n            <Badge key={v} tone=\"brand\" onRemove={disabled ? undefined : () => set(chosen.filter((x) => x !== v))} removeLabel={remove(labelOf(v))}>{labelOf(v)}</Badge>\n          ))}\n          <input\n            ref={input}\n            id={inputId}\n            className=\"k-tag-input__control\"\n            role=\"combobox\"\n            aria-autocomplete=\"list\"\n            aria-expanded={open}\n            aria-controls={listId}\n            aria-activedescendant={open && active >= 0 ? `${listId}-${active}` : undefined}\n            aria-describedby={describedBy(inputId, hint, error, `${inputId}-count`)}\n            aria-invalid={error ? true : undefined}\n            autoComplete=\"off\"\n            placeholder={chosen.length ? undefined : placeholder}\n            disabled={disabled}\n            value={query}\n            onChange={(e) => {\n              setQuery(e.target.value);\n              setOpen(true);\n              setActive(0);\n            }}\n            onFocus={() => setOpen(true)}\n            onKeyDown={onKey}\n          />\n          {chosen.length > 1 && !disabled && (\n            <button type=\"button\" className=\"k-input__action\" aria-label={clear} onClick={() => { set([]); input.current?.focus(); }}>\n              <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.7} strokeLinecap=\"round\" aria-hidden=\"true\"><path d=\"m6 6 8 8M14 6l-8 8\" /></svg>\n            </button>\n          )}\n        </div>\n        <ul id={listId} role=\"listbox\" aria-multiselectable=\"true\" aria-label={typeof label === \"string\" ? label : undefined} className=\"k-listbox\" hidden={!open} style={{ insetBlockStart: \"calc(100% + 6px)\" }}>\n          {filtered.length === 0 && <li className=\"k-listbox__empty\" role=\"presentation\">{emptyMessage}</li>}\n          {filtered.map((o, i) => {\n            const on = chosen.includes(o.value);\n            return (\n              <li\n                key={o.value}\n                id={`${listId}-${i}`}\n                role=\"option\"\n                aria-selected={on}\n                aria-disabled={o.disabled || (!on && full) || undefined}\n                data-active={i === active ? \"\" : undefined}\n                className=\"k-option k-option--check\"\n                onPointerDown={(e) => e.preventDefault()}\n                onClick={() => toggle(o)}\n                onPointerMove={() => active !== i && setActive(i)}\n              >\n                <span className=\"k-multiselect__box\" aria-hidden=\"true\">{on && <Check />}</span>\n                {o.icon}\n                <span className=\"k-option__body\">\n                  <span>{highlight(o.label, query)}</span>\n                  {o.description && <span className=\"k-option__desc\">{o.description}</span>}\n                </span>\n              </li>\n            );\n          })}\n        </ul>\n        <span id={`${inputId}-count`} className=\"k-sr-only\">{selectedLabel(chosen.length)}</span>\n        <span className=\"k-sr-only\" aria-live=\"polite\">{announce}</span>\n        {name && chosen.map((v) => <input key={v} type=\"hidden\" name={name} value={v} />)}\n      </div>\n    </Field>\n  );\n}\n"
    }
  ]
}