{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-thread-search",
  "type": "registry:component",
  "title": "Thread search",
  "description": "History you can actually get back into: pinned first, then grouped by when.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/thread-search.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/thread-search.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { PinIcon, SearchIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { field, mono, paper } from \"./surfaces\";\n\nexport interface SearchableThread {\n  id: string;\n  title: string;\n  group: string;\n  preview: string;\n  pinned?: boolean;\n}\n\nexport function ThreadSearch({\n  threads,\n  query,\n  activeId,\n  onQueryChange,\n  onSelect,\n  className,\n  ...props\n}: Omit<\n  ComponentProps<\"div\">,\n  \"children\" | \"threads\" | \"query\" | \"activeId\" | \"onQueryChange\" | \"onSelect\"\n> & {\n  threads: readonly SearchableThread[];\n  query: string;\n  activeId: string;\n  onQueryChange?: (query: string) => void;\n  onSelect?: (id: string) => void;\n}) {\n  const matches = threads.filter((thread) =>\n    `${thread.title} ${thread.preview}`\n      .toLowerCase()\n      .includes(query.toLowerCase()),\n  );\n  const pinned = matches.filter((thread) => thread.pinned);\n  const groups = [\n    ...new Set(matches.filter((t) => !t.pinned).map((t) => t.group)),\n  ];\n\n  const ordered = [\n    ...pinned,\n    ...groups.flatMap((group) =>\n      matches.filter((thread) => !thread.pinned && thread.group === group),\n    ),\n  ];\n\n  const move = (delta: number) => {\n    if (ordered.length === 0) return;\n    const at = ordered.findIndex((thread) => thread.id === activeId);\n    // activeId can be filtered out by the query; start from the edge the key implies\n    const from = at === -1 ? (delta > 0 ? -1 : 0) : at;\n    const next = ordered[(from + delta + ordered.length) % ordered.length];\n    if (next) onSelect?.(next.id);\n  };\n\n  const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (event.nativeEvent.isComposing) return;\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      move(1);\n    } else if (event.key === \"ArrowUp\") {\n      event.preventDefault();\n      move(-1);\n    }\n  };\n\n  const row = (thread: SearchableThread) => (\n    <button\n      key={thread.id}\n      type=\"button\"\n      onClick={() => onSelect?.(thread.id)}\n      className={cn(\n        \"flex flex-col gap-0.5 rounded-xl px-2 py-1 text-start transition-colors\",\n        thread.id === activeId\n          ? \"bg-foreground/[0.05]\"\n          : \"hover:bg-foreground/[0.03]\",\n      )}\n    >\n      <span className=\"flex items-center gap-1.5\">\n        {thread.pinned && (\n          <PinIcon className=\"text-foreground/30 size-2.5 shrink-0\" />\n        )}\n        <span className=\"min-w-0 flex-1 truncate text-[13px]\">\n          {thread.title}\n        </span>\n      </span>\n      <span className=\"text-foreground/35 truncate text-xs\">\n        {thread.preview}\n      </span>\n    </button>\n  );\n\n  return (\n    <div\n      data-slot=\"thread-search\"\n      className={cn(\n        paper,\n        \"flex w-full max-w-sm flex-col gap-1.5 rounded-2xl p-3\",\n        className,\n      )}\n\n      {...props}\n    >\n      <div\n        className={cn(\n          field,\n          \"flex items-center gap-2 rounded-xl px-2.5 py-1.5\",\n        )}\n      >\n        <SearchIcon className=\"text-foreground/30 size-3.5 shrink-0\" />\n        <input\n          value={query}\n          onChange={(event) => onQueryChange?.(event.target.value)}\n          onKeyDown={onKeyDown}\n          placeholder=\"Search threads\"\n          aria-label=\"Search threads\"\n          className=\"text-foreground/85 placeholder:text-foreground/30 min-w-0 flex-1 bg-transparent text-[13px] outline-none\"\n        />\n      </div>\n\n      {pinned.length > 0 && (\n        <div className=\"flex flex-col\">\n          <span className={cn(mono, \"text-foreground/25 px-2 pb-1\")}>\n            pinned\n          </span>\n          {pinned.map(row)}\n        </div>\n      )}\n\n      {groups.map((group) => (\n        <div key={group} className=\"flex flex-col\">\n          <span className={cn(mono, \"text-foreground/25 px-2 pb-1\")}>\n            {group}\n          </span>\n          {matches\n            .filter((thread) => !thread.pinned && thread.group === group)\n            .map(row)}\n        </div>\n      ))}\n\n      {matches.length === 0 && (\n        <span className=\"text-foreground/30 px-2 py-4 text-center text-xs\">\n          No thread matches “{query}”\n        </span>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}