{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-suggest",
  "type": "registry:ui",
  "title": "SearchSuggest",
  "description": "جعبهٔ جستجو با جستجوهای اخیر و پرطرفدار و پیشنهاد هنگام تایپ (الگوی فروشگاه‌های اینترنتی)",
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json",
    "https://kashi.sepantapouya.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/kashi/SearchSuggest.tsx",
      "type": "registry:ui",
      "target": "components/kashi/SearchSuggest.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useId, useMemo, useRef, useState, type FormEvent, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cx } from \"./cx\";\nimport { useKashi } from \"./KashiProvider\";\nimport { useIsomorphicLayoutEffect } from \"./utils\";\n\nexport interface SearchSuggestProps {\n  value?: string;\n  defaultValue?: string;\n  onChange?: (query: string) => void;\n  /** با Enter، انتخاب پیشنهاد یا زدن یک جستجوی اخیر/پرطرفدار */\n  onSearch?: (query: string) => void;\n  /** جستجوهای اخیر (کنترل‌شده)؛ یا به‌جایش storageKey بدهید تا خودکار در مرورگر ذخیره شوند */\n  recent?: string[];\n  onClearRecent?: () => void;\n  /** کلید localStorage برای ذخیرهٔ خودکار جستجوهای اخیر (حداکثر ۸ مورد) */\n  storageKey?: string;\n  /** جستجوهای پرطرفدار، با آیکون روند */\n  popular?: string[];\n  /** پیشنهادها هنگام تایپ؛ آرایه (خودکار فیلتر می‌شود) یا تابعی که برای هر عبارت فهرست می‌دهد */\n  suggestions?: string[] | ((query: string) => string[]);\n  placeholder?: string;\n  /** برچسب برای صفحه‌خوان @default «جستجو» */\n  label?: string;\n  name?: string;\n  className?: string;\n  labels?: { recent?: ReactNode; popular?: ReactNode; clear?: ReactNode };\n  /** پیشنهادها در راه‌اند (مثلاً از سرور)؛ ردیف‌های اسکلتی زیر جعبه */\n  loading?: boolean;\n  /** پیام وقتی برای عبارت تایپ‌شده پیشنهادی نیست؛ بدون آن، پنل بسته می‌ماند */\n  emptyMessage?: ReactNode;\n}\n\nconst norm = (s: string) => s.replace(/[يى]/g, \"ی\").replace(/ك/g, \"ک\").replace(/[‌\\s]+/g, \" \").trim().toLowerCase();\n\nconst SearchIcon = () => <svg viewBox=\"0 0 20 20\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" aria-hidden=\"true\"><circle cx=\"9.1\" cy=\"9.1\" r=\"6.2\" /><path d=\"m14 14 3.3 3.3\" /></svg>;\nconst TrendIcon = () => <svg viewBox=\"0 0 20 20\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"m2.75 13.5 4.6-4.6 3.2 3.2 6.2-6.2M12.75 5.9h4v4\" /></svg>;\nconst HistoryIcon = () => <svg viewBox=\"0 0 20 20\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\"><path d=\"M3.3 10a6.7 6.7 0 1 0 1.9-4.7M3.2 2.9v2.6h2.6M10 6.6V10l2.3 1.5\" /></svg>;\n\n/** جعبهٔ جستجو با جستجوهای اخیر و پرطرفدار و پیشنهاد هنگام تایپ (الگوی فروشگاه‌های اینترنتی) */\nexport function SearchSuggest({\n  value, defaultValue, onChange, onSearch, recent, onClearRecent, storageKey, popular = [], suggestions,\n  placeholder, label, name, className, labels = {}, loading, emptyMessage,\n}: SearchSuggestProps) {\n  const { messages: M } = useKashi();\n  const id = useId();\n  const listId = `${id}-panel`;\n  const [inner, setInner] = useState(defaultValue ?? \"\");\n  const q = value ?? inner;\n  const setQ = (s: string) => { if (value === undefined) setInner(s); onChange?.(s); };\n\n  // جستجوهای اخیر: کنترل‌شده با recent، یا ذخیرهٔ خودکار با storageKey\n  const [stored, setStored] = useState<string[]>([]);\n  useEffect(() => {\n    if (!storageKey) return;\n    try { setStored(JSON.parse(localStorage.getItem(storageKey) ?? \"[]\")); } catch { /* حالت خصوصی */ }\n  }, [storageKey]);\n  const history = recent ?? stored;\n  const remember = (s: string) => {\n    if (!storageKey || recent) return;\n    const next = [s, ...stored.filter((x) => norm(x) !== norm(s))].slice(0, 8);\n    setStored(next);\n    try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch { /* حالت خصوصی */ }\n  };\n  const clearHistory = () => {\n    if (storageKey && !recent) { setStored([]); try { localStorage.removeItem(storageKey); } catch { /* */ } }\n    onClearRecent?.();\n  };\n\n  const list = useMemo(() => {\n    if (!q.trim() || !suggestions) return [];\n    if (typeof suggestions === \"function\") return suggestions(q).slice(0, 8);\n    const n = norm(q);\n    return suggestions.filter((s) => norm(s).includes(n)).slice(0, 8);\n  }, [q, suggestions]);\n\n  const [open, setOpen] = useState(false);\n  const [active, setActive] = useState(-1);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const boxRef = useRef<HTMLDivElement>(null);\n  const showList = list.length > 0;\n  const showIdle = !q.trim() && (history.length > 0 || popular.length > 0);\n  const showLoading = !!loading && !!q.trim() && !showList;\n  const showEmpty = !loading && !!q.trim() && !showList && emptyMessage !== undefined;\n  const visible = open && (showList || showIdle || showLoading || showEmpty);\n\n  const submit = (s: string) => {\n    const t = s.trim();\n    if (!t) return;\n    setQ(t);\n    remember(t);\n    onSearch?.(t);\n    setOpen(false);\n    setActive(-1);\n  };\n\n  // پنل با Popover API در لایهٔ بالا باز می‌شود تا ظرف‌های overflow: hidden آن را نبُرند\n  const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);\n  const place = useCallback(() => {\n    const r = boxRef.current?.getBoundingClientRect();\n    if (r) setPos({ top: r.bottom + 8, left: r.left, width: r.width });\n  }, []);\n  useIsomorphicLayoutEffect(() => {\n    const el = panelRef.current as (HTMLDivElement & { showPopover?: () => void; hidePopover?: () => void }) | null;\n    if (!visible) { if (el?.matches?.(\":popover-open\")) el.hidePopover?.(); return; }\n    if (el?.showPopover && !el.matches(\":popover-open\")) el.showPopover();\n    place();\n    addEventListener(\"resize\", place);\n    addEventListener(\"scroll\", place, true);\n    return () => { removeEventListener(\"resize\", place); removeEventListener(\"scroll\", place, true); };\n  }, [visible, place]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onDown = (e: PointerEvent) => {\n      const t = e.target as Node;\n      if (!panelRef.current?.contains(t) && !boxRef.current?.contains(t)) setOpen(false);\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [open]);\n  useEffect(() => setActive(-1), [q]);\n\n  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Escape\") { if (open) { e.preventDefault(); setOpen(false); } else if (q) setQ(\"\"); return; }\n    if (!showList) return;\n    if (e.key === \"ArrowDown\") { e.preventDefault(); setOpen(true); setActive((a) => (a + 1) % list.length); }\n    else if (e.key === \"ArrowUp\") { e.preventDefault(); setActive((a) => (a <= 0 ? list.length - 1 : a - 1)); }\n  };\n  const onSubmit = (e: FormEvent) => { e.preventDefault(); submit(active >= 0 && list[active] ? list[active] : q); };\n\n  const chip = (s: string, kind: \"recent\" | \"popular\") => (\n    <li key={`${kind}-${s}`}>\n      <button type=\"button\" className=\"k-search-suggest__chip\" onMouseDown={(e) => e.preventDefault()} onClick={() => submit(s)}>\n        {kind === \"popular\" ? <TrendIcon /> : <HistoryIcon />}{s}\n      </button>\n    </li>\n  );\n\n  const highlight = (s: string) => {\n    const i = norm(s).indexOf(norm(q));\n    if (i < 0 || s.length !== norm(s).length) return s;\n    return <>{s.slice(0, i)}<mark>{s.slice(i, i + q.trim().length)}</mark>{s.slice(i + q.trim().length)}</>;\n  };\n\n  return (\n    <form role=\"search\" className={cx(\"k-search-suggest\", className)} onSubmit={onSubmit} data-open={visible || undefined}>\n      <div className=\"k-search-suggest__box\" ref={boxRef} aria-busy={loading || undefined}>\n        <SearchIcon />\n        <input\n          ref={inputRef}\n          type=\"search\"\n          name={name}\n          className=\"k-search-suggest__input\"\n          value={q}\n          placeholder={placeholder}\n          aria-label={label ?? M.searchSuggest.label}\n          role=\"combobox\"\n          aria-autocomplete=\"list\"\n          aria-expanded={visible}\n          aria-controls={listId}\n          aria-activedescendant={visible && showList && active >= 0 ? `${listId}-${active}` : undefined}\n          autoComplete=\"off\"\n          enterKeyHint=\"search\"\n          onFocus={() => setOpen(true)}\n          onChange={(e) => { setQ(e.target.value); setOpen(true); }}\n          onKeyDown={onKeyDown}\n        />\n      </div>\n      <div\n        id={listId}\n        ref={panelRef}\n        className=\"k-search-suggest__panel\"\n        hidden={!visible}\n        {...({ popover: \"manual\" } as Record<string, string>)}\n        style={pos ? { position: \"fixed\", inset: \"auto\", margin: 0, top: pos.top, left: pos.left, width: pos.width } : undefined}\n        onKeyDown={(e) => { if (e.key === \"Escape\") { setOpen(false); inputRef.current?.focus(); } }}\n      >\n        {showLoading ? (\n          <ul className=\"k-search-suggest__list\" role=\"presentation\">{[0, 1, 2].map((k) => <li key={k} className=\"k-listbox__loading\" />)}</ul>\n        ) : showEmpty ? (\n          <ul className=\"k-search-suggest__list\" role=\"presentation\"><li className=\"k-listbox__empty\">{emptyMessage}</li></ul>\n        ) : showList ? (\n          <ul role=\"listbox\" aria-label={M.searchSuggest.suggestions} className=\"k-search-suggest__list\">\n            {list.map((s, i) => (\n              <li key={s} id={`${listId}-${i}`} role=\"option\" aria-selected={i === active} className=\"k-option\" data-active={i === active || undefined}\n                onMouseDown={(e) => e.preventDefault()} onPointerMove={() => setActive(i)} onClick={() => submit(s)}>\n                <SearchIcon /><span className=\"k-option__label\">{highlight(s)}</span>\n              </li>\n            ))}\n          </ul>\n        ) : (\n          <>\n            {history.length > 0 && (\n              <section className=\"k-search-suggest__section\">\n                <div className=\"k-search-suggest__head\">\n                  <b>{labels.recent ?? M.searchSuggest.recent}</b>\n                  <button type=\"button\" className=\"k-btn k-btn--link k-btn--sm\" onMouseDown={(e) => e.preventDefault()} onClick={clearHistory}>{labels.clear ?? M.searchSuggest.clear}</button>\n                </div>\n                <ul className=\"k-search-suggest__chips\">{history.map((s) => chip(s, \"recent\"))}</ul>\n              </section>\n            )}\n            {popular.length > 0 && (\n              <section className=\"k-search-suggest__section\">\n                <div className=\"k-search-suggest__head\"><b>{labels.popular ?? M.searchSuggest.popular}</b></div>\n                <ul className=\"k-search-suggest__chips\">{popular.map((s) => chip(s, \"popular\"))}</ul>\n              </section>\n            )}\n          </>\n        )}\n      </div>\n    </form>\n  );\n}\n"
    }
  ]
}