{ "name": "context-picker", "type": "registry:ui", "title": "Context Picker", "description": "Picks what an assistant can see. A dropdown rather than a dialog, because attaching context happens mid-thought and a modal takes focus off the prompt you were writing.", "category": "AI", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "button", "dropdown-menu", "input", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/context-picker.tsx", "type": "registry:ui", "content": "'use client'\n\nimport { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { Check, FileCode, Plus, Search } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Input } from '@/components/ui/input'\nimport { menuItem, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * Picks what an assistant can see: files, selections, docs, tools.\n *\n * A dropdown rather than a dialog, because attaching context happens mid-thought\n * — a modal would take focus away from the prompt you were part-way through\n * writing. Already-attached sources stay in the list with a tick rather than\n * disappearing, so the menu does not reshuffle under the pointer.\n */\nexport type ContextSource = {\n id: string\n label: string\n group?: string\n detail?: string\n icon?: ReactNode\n disabled?: boolean\n}\n\nfunction ContextPicker({\n sources,\n selected = [],\n onSelect,\n onDeselect,\n trigger,\n emptyMessage = 'Nothing matches',\n triggerLabel = 'Context',\n searchPlaceholder = 'Search files and docs',\n searchLabel = 'Search context sources',\n ...props\n // 'onSelect' is also a DOM event on the underlying div; omitting it stops the\n // native signature from shadowing this one.\n}: Omit, 'children' | 'onSelect'> & {\n sources: ContextSource[]\n selected?: string[]\n onSelect?: (id: string) => void\n onDeselect?: (id: string) => void\n trigger?: ReactNode\n emptyMessage?: string\n /** Text on the default trigger. Ignored when `trigger` is given. */\n triggerLabel?: ReactNode\n searchPlaceholder?: string\n /** Accessible name for the search field. */\n searchLabel?: string\n}) {\n const [query, setQuery] = useState('')\n\n const groups = useMemo(() => {\n const needle = query.trim().toLowerCase()\n const matched = needle\n ? sources.filter((s) =>\n `${s.label} ${s.detail ?? ''} ${s.group ?? ''}`\n .toLowerCase()\n .includes(needle),\n )\n : sources\n\n const map = new Map()\n for (const source of matched) {\n const key = source.group ?? ''\n if (!map.has(key)) map.set(key, [])\n map.get(key)!.push(source)\n }\n return [...map.entries()]\n }, [sources, query])\n\n return (\n \n \n {trigger ?? (\n \n )}\n \n\n \n
\n }\n placeholder={searchPlaceholder}\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n aria-label={searchLabel}\n />\n
\n\n
\n {groups.length === 0 && (\n

\n {emptyMessage}\n

\n )}\n\n {groups.map(([group, items]) => (\n
\n {group && {group}}\n {items.map((source) => {\n const on = selected.includes(source.id)\n return (\n (on ? onDeselect?.(source.id) : onSelect?.(source.id))}\n className={cn(menuItem, radius.control, 'w-full')}\n >\n {source.icon ?? }\n \n {source.label}\n \n {source.detail && (\n \n {source.detail}\n \n )}\n {on && }\n \n )\n })}\n
\n ))}\n
\n\n {selected.length > 0 && (\n <>\n \n
\n {selected.length} attached\n \n {selected.length}\n \n
\n \n )}\n
\n
\n )\n}\n\nexport { ContextPicker }\n" } ] }