{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-prompt-library",
  "type": "registry:component",
  "title": "Prompt library",
  "description": "Prompts you saved, searchable, with their variables shown before you insert one.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/prompt-library.tsx",
      "content": "\"use client\";\n\nimport { useId, type ComponentProps } from \"react\";\nimport { BookmarkIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { field, mono, paper } from \"./surfaces\";\n\nexport interface SavedPrompt {\n  id: string;\n  name: string;\n  body: string;\n  variables: readonly string[];\n}\n\nexport function PromptLibrary({\n  prompts,\n  query,\n  selectedId,\n  onQueryChange,\n  onSelect,\n  onInsert,\n  className,\n  ...props\n}: Omit<\n  ComponentProps<\"div\">,\n  | \"children\"\n  | \"prompts\"\n  | \"query\"\n  | \"selectedId\"\n  | \"onQueryChange\"\n  | \"onSelect\"\n  | \"onInsert\"\n> & {\n  prompts: readonly SavedPrompt[];\n  query: string;\n  selectedId: string;\n  onQueryChange?: (query: string) => void;\n  onSelect?: (id: string) => void;\n  onInsert?: (id: string) => void;\n}) {\n  const listId = useId();\n  const optionId = (id: string) => `${listId}-${id}`;\n  const matches = prompts.filter((prompt) =>\n    prompt.name.toLowerCase().includes(query.toLowerCase()),\n  );\n  const selected = matches.find((prompt) => prompt.id === selectedId);\n\n  const move = (delta: number) => {\n    if (matches.length === 0) return;\n    const at = matches.findIndex((prompt) => prompt.id === selectedId);\n    // selectedId 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 = matches[(from + delta + matches.length) % matches.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    } else if (event.key === \"Enter\" && selected) {\n      event.preventDefault();\n      onInsert?.(selected.id);\n    }\n  };\n\n  return (\n    <div\n      data-slot=\"prompt-library\"\n      className={cn(\n        paper,\n        \"flex w-full max-w-sm flex-col gap-2 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        <BookmarkIcon 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 prompts\"\n          aria-label=\"Search saved prompts\"\n          role=\"combobox\"\n          aria-expanded={matches.length > 0}\n          aria-controls={listId}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={selected ? optionId(selected.id) : undefined}\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      <div\n        id={listId}\n        role=\"listbox\"\n        aria-label=\"Saved prompts\"\n        className=\"flex flex-col\"\n      >\n        {matches.map((prompt) => (\n          <button\n            key={prompt.id}\n            id={optionId(prompt.id)}\n            type=\"button\"\n            role=\"option\"\n            tabIndex={-1}\n            aria-selected={prompt.id === selectedId}\n            onMouseDown={(event) => event.preventDefault()}\n            onClick={() => onSelect?.(prompt.id)}\n            onDoubleClick={() => onInsert?.(prompt.id)}\n            className={cn(\n              \"flex items-center gap-2 rounded-xl px-2 py-1.5 text-start transition-colors\",\n              prompt.id === selectedId\n                ? \"bg-foreground/[0.05]\"\n                : \"hover:bg-foreground/[0.03]\",\n            )}\n          >\n            <span className=\"min-w-0 flex-1 truncate text-[13px]\">\n              {prompt.name}\n            </span>\n            {prompt.variables.length > 0 && (\n              <span className={cn(mono, \"text-foreground/25 shrink-0\")}>\n                {prompt.variables.length} vars\n              </span>\n            )}\n          </button>\n        ))}\n      </div>\n      {matches.length === 0 && (\n        <span className=\"text-foreground/30 block px-2 py-3 text-center text-xs break-words\">\n          Nothing matches “{query}”\n        </span>\n      )}\n\n      {selected && (\n        <div\n          className={cn(\n            field,\n            \"fade-in animate-in flex flex-col gap-2 rounded-xl p-2.5 duration-200\",\n          )}\n        >\n          <p className=\"text-foreground/65 text-xs leading-relaxed break-words\">\n            {selected.body}\n          </p>\n          {selected.variables.length > 0 && (\n            <div className=\"flex flex-wrap gap-1\">\n              {selected.variables.map((variable) => (\n                <span\n                  key={variable}\n                  className={cn(\n                    mono,\n                    \"bg-background/70 text-foreground/50 rounded px-1.5 py-0.5\",\n                  )}\n                >\n                  {`{${variable}}`}\n                </span>\n              ))}\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}