{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "autocomplete",
	"title": "Autocomplete",
	"type": "registry:ui",
	"description": "An input component with autocomplete suggestions.",
	"dependencies": [
		"@lucide/svelte@^0.554.0"
	],
	"devDependencies": [
		"@lucide/svelte@^0.544.0"
	],
	"registryDependencies": [
		"input"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" generics=\"T\">\r\n\timport { Input } from '$lib/components/ui/input';\r\n\timport { cn } from '$UTILS$';\r\n\timport { Loader2, Search, X } from '@lucide/svelte';\r\n\timport { fly } from 'svelte/transition';\r\n\timport type { Snippet } from 'svelte';\r\n\r\n\tlet {\r\n\t\tvalue = $bindable(''),\r\n\t\tselected = $bindable<T | null>(null),\r\n\t\toptions = [],\r\n\t\tloading = false,\r\n\t\tplaceholder = 'Search...',\r\n\t\tlabelKey,\r\n\t\tclass: className,\r\n\t\tonSelect,\r\n\t\titemSnippet,\r\n\t\temptySnippet,\r\n\t\t...rest\r\n\t}: {\r\n\t\tvalue: string;\r\n\t\tselected?: T | null;\r\n\t\toptions: T[];\r\n\t\tloading?: boolean;\r\n\t\tplaceholder?: string;\r\n\t\tlabelKey?: keyof T;\r\n\t\tclass?: string;\r\n\t\tonSelect?: (item: T) => void;\r\n\t\titemSnippet: Snippet<[T, boolean]>;\r\n\t\temptySnippet?: Snippet;\r\n\t\t[key: string]: any;\r\n\t} = $props();\r\n\r\n\tlet isOpen = $state(false);\r\n\tlet activeIndex = $state(-1);\r\n\r\n\tlet inputContainerRef = $state<HTMLDivElement | null>(null);\r\n\tlet listRef = $state<HTMLDivElement | null>(null);\r\n\r\n\tfunction open() {\r\n\t\tisOpen = true;\r\n\t\tactiveIndex = -1;\r\n\t}\r\n\r\n\tfunction close() {\r\n\t\tisOpen = false;\r\n\t\tactiveIndex = -1;\r\n\t}\r\n\r\n\tfunction handleInput(e: Event) {\r\n\t\tvalue = (e.target as HTMLInputElement).value;\r\n\t\tif (value.length > 0) open();\r\n\t\telse close();\r\n\t}\r\n\r\n\tfunction handleSelect(item: T) {\r\n\t\tselected = item;\r\n\r\n\t\tif (labelKey && item && typeof item === 'object') {\r\n\t\t\tvalue = String(item[labelKey]);\r\n\t\t} else if (typeof item === 'string') {\r\n\t\t\tvalue = item;\r\n\t\t}\r\n\r\n\t\tif (onSelect) onSelect(item);\r\n\t\tclose();\r\n\t}\r\n\r\n\tfunction handleKeydown(e: KeyboardEvent) {\r\n\t\tif (!isOpen) {\r\n\t\t\tif (e.key === 'ArrowDown' && value.length > 0) open();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tswitch (e.key) {\r\n\t\t\tcase 'ArrowDown':\r\n\t\t\t\te.preventDefault();\r\n\t\t\t\tactiveIndex = (activeIndex + 1) % options.length;\r\n\t\t\t\tscrollToActive();\r\n\t\t\t\tbreak;\r\n\t\t\tcase 'ArrowUp':\r\n\t\t\t\te.preventDefault();\r\n\t\t\t\tactiveIndex = (activeIndex - 1 + options.length) % options.length;\r\n\t\t\t\tscrollToActive();\r\n\t\t\t\tbreak;\r\n\t\t\tcase 'Enter':\r\n\t\t\t\te.preventDefault();\r\n\t\t\t\tif (activeIndex >= 0 && options[activeIndex]) {\r\n\t\t\t\t\thandleSelect(options[activeIndex]);\r\n\t\t\t\t}\r\n\t\t\t\tbreak;\r\n\t\t\tcase 'Escape':\r\n\t\t\t\tclose();\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tfunction scrollToActive() {\r\n\t\tif (!listRef) return;\r\n\t\tconst activeEl = listRef.children[activeIndex] as HTMLElement;\r\n\t\tif (activeEl) {\r\n\t\t\tactiveEl.scrollIntoView({ block: 'nearest' });\r\n\t\t}\r\n\t}\r\n\r\n\tfunction clickOutside(node: HTMLElement) {\r\n\t\tconst handleClick = (event: MouseEvent) => {\r\n\t\t\tconst target = event.target as Node;\r\n\t\t\tif (\r\n\t\t\t\tnode &&\r\n\t\t\t\t!node.contains(target) &&\r\n\t\t\t\tinputContainerRef &&\r\n\t\t\t\t!inputContainerRef.contains(target)\r\n\t\t\t) {\r\n\t\t\t\tclose();\r\n\t\t\t}\r\n\t\t};\r\n\t\tdocument.addEventListener('click', handleClick, true);\r\n\t\treturn {\r\n\t\t\tdestroy() {\r\n\t\t\t\tdocument.removeEventListener('click', handleClick, true);\r\n\t\t\t}\r\n\t\t};\r\n\t}\r\n</script>\r\n\r\n<div class={cn('relative w-full group', className)}>\r\n\t<div class=\"relative\" bind:this={inputContainerRef}>\r\n\t\t<Input\r\n\t\t\ttype=\"text\"\r\n\t\t\t{placeholder}\r\n\t\t\tbind:value\r\n\t\t\toninput={handleInput}\r\n\t\t\tonkeydown={handleKeydown}\r\n\t\t\tonfocus={() => {\r\n\t\t\t\tif (value) open();\r\n\t\t\t}}\r\n\t\t\trole=\"combobox\"\r\n\t\t\taria-expanded={isOpen}\r\n\t\t\taria-autocomplete=\"list\"\r\n\t\t\t{...rest}\r\n\t\t/>\r\n\r\n\t\t{#if loading}\r\n\t\t\t<Loader2\r\n\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground\"\r\n\t\t\t/>\r\n\t\t{:else if value.length > 0}\r\n\t\t\t<button\r\n\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\r\n\t\t\t\tonclick={() => {\r\n\t\t\t\t\tvalue = '';\r\n\t\t\t\t\tselected = null;\r\n\t\t\t\t\tinputContainerRef?.querySelector('input')?.focus();\r\n\t\t\t\t\tclose();\r\n\t\t\t\t}}\r\n\t\t\t\taria-label=\"Clear\"\r\n\t\t\t>\r\n\t\t\t\t<X class=\"h-4 w-4\" />\r\n\t\t\t</button>\r\n\t\t{:else}\r\n\t\t\t<Search\r\n\t\t\t\tclass=\"absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none\"\r\n\t\t\t/>\r\n\t\t{/if}\r\n\t</div>\r\n\r\n\t{#if isOpen}\r\n\t\t<div\r\n\t\t\tuse:clickOutside\r\n\t\t\tbind:this={listRef}\r\n\t\t\tclass=\"absolute z-50 mt-2 w-full max-h-[300px] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none\"\r\n\t\t\ttransition:fly={{ y: -5, duration: 150 }}\r\n\t\t>\r\n\t\t\t{#if options.length > 0}\r\n\t\t\t\t{#each options as option, i}\r\n\t\t\t\t\t<div\r\n\t\t\t\t\t\trole=\"option\"\r\n\t\t\t\t\t\ttabindex=\"-1\"\r\n\t\t\t\t\t\taria-selected={i === activeIndex}\r\n\t\t\t\t\t\tclass={cn(\r\n\t\t\t\t\t\t\t'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',\r\n\t\t\t\t\t\t\ti === activeIndex\r\n\t\t\t\t\t\t\t\t? 'bg-accent text-accent-foreground'\r\n\t\t\t\t\t\t\t\t: 'hover:bg-accent hover:text-accent-foreground'\r\n\t\t\t\t\t\t)}\r\n\t\t\t\t\t\tonclick={() => handleSelect(option)}\r\n\t\t\t\t\t\tonmouseenter={() => (activeIndex = i)}\r\n\t\t\t\t\t>\r\n\t\t\t\t\t\t{@render itemSnippet(option, i === activeIndex)}\r\n\t\t\t\t\t</div>\r\n\t\t\t\t{/each}\r\n\t\t\t{:else if !loading}\r\n\t\t\t\t<div class=\"py-6 text-center text-sm text-muted-foreground\">\r\n\t\t\t\t\t{#if emptySnippet}\r\n\t\t\t\t\t\t{@render emptySnippet()}\r\n\t\t\t\t\t{:else}\r\n\t\t\t\t\t\tNo results found.\r\n\t\t\t\t\t{/if}\r\n\t\t\t\t</div>\r\n\t\t\t{/if}\r\n\t\t</div>\r\n\t{/if}\r\n</div>\r\n",
			"type": "registry:ui",
			"target": "autocomplete/autocomplete.svelte"
		},
		{
			"content": "<script lang=\"ts\">\r\n    let { text, query, class: className }: { text: string, query: string, class?: string } = $props();\r\n\r\n    let parts = $derived.by(() => {\r\n        if (!query) return [{ text, highlight: false }];\r\n        const regex = new RegExp(`(${query})`, 'gi');\r\n        return text.split(regex).map(part => ({\r\n            text: part,\r\n            highlight: part.toLowerCase() === query.toLowerCase()\r\n        }));\r\n    });\r\n</script>\r\n\r\n<span class={className}>\r\n    {#each parts as part}\r\n        {#if part.highlight}\r\n            <span class=\"bg-yellow-200 dark:bg-yellow-900/50 font-semibold text-foreground\">{part.text}</span>\r\n        {:else}\r\n            {part.text}\r\n        {/if}\r\n    {/each}\r\n</span>",
			"type": "registry:ui",
			"target": "autocomplete/autocomplete-highlight.svelte"
		},
		{
			"content": "import Root from \"./autocomplete.svelte\";\r\nimport Highlight from './autocomplete-highlight.svelte'\r\n\r\nexport {\r\n    Root,\r\n    Highlight\r\n};",
			"type": "registry:ui",
			"target": "autocomplete/index.ts"
		}
	]
}