AI Command Palette
A centralized, modal-based search bar (Cmd+K) that executes commands and features streaming AI text responses, keyboard navigation with spring animations, and a premium glass aesthetic.
Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
npx -y vui-registry-cli-v1@latest add ai-command-paletteSource Code
'use client'
import { useState, useEffect } from 'react'
import { Command } from 'cmdk'
import { motion, AnimatePresence } from 'framer-motion'
import { useTheme } from 'next-themes'
import { Search, Bot, Wand2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { PaletteTrigger } from './palette-trigger'
import { PaletteFooter } from './palette-footer'
import { CommandMode } from './command-mode'
import { AiMode } from './ai-mode'
export default function AICommandPalette() {
const { theme, setTheme } = useTheme()
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const [mode, setMode] = useState<'command' | 'ai'>('command')
const [aiResponse, setAiResponse] = useState('')
const [isTyping, setIsTyping] = useState(false)
const [notification, setNotification] = useState({ message: '', visible: false })
const showNotification = (message: string) => {
setNotification({ message, visible: true })
setTimeout(() => setNotification((prev) => ({ ...prev, visible: false })), 2000)
}
const handleCommandSelect = (id: string) => {
switch (id) {
case 'ask-ai':
setMode('ai')
setSearch('')
break
case 'search-docs':
showNotification('Opening documentation...')
setTimeout(() => setOpen(false), 800)
break
case 'theme-light':
setTheme('light')
showNotification('Switched to Light Mode')
break
case 'theme-dark':
setTheme('dark')
showNotification('Switched to Dark Mode')
break
case 'theme-system':
setTheme('system')
showNotification('Using System Theme')
break
case 'settings':
showNotification('Opening settings panel...')
setTimeout(() => setOpen(false), 800)
break
case 'home':
showNotification('Navigating to Dashboard...')
setTimeout(() => setOpen(false), 800)
break
case 'projects':
showNotification('Loading Projects...')
setTimeout(() => setOpen(false), 800)
break
default:
setOpen(false)
}
}
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((o) => !o)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
useEffect(() => {
if (mode === 'ai' && search.length > 5 && !isTyping) {
const timer = setTimeout(() => {
setIsTyping(true)
setAiResponse('')
const response =
"Based on your request, I can help you configure the component. The 'ProScheduler' supports custom event types, drag-and-drop interactions, and premium theming options. Would you like me to generate a configuration template?"
let i = 0
const interval = setInterval(() => {
setAiResponse((prev) => prev + response.charAt(i))
i++
if (i >= response.length) {
clearInterval(interval)
setIsTyping(false)
}
}, 30)
return () => clearInterval(interval)
}, 1000)
return () => clearTimeout(timer)
}
}, [mode, search, isTyping])
useEffect(() => {
if (!open) {
setTimeout(() => {
setMode('command')
setSearch('')
setAiResponse('')
}, 200)
}
}, [open])
return (
<div className="h-[600px] w-full flex flex-col items-center justify-center bg-neutral-100 dark:bg-neutral-950 p-4 relative overflow-hidden font-sans">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808008_1px,transparent_1px),linear-gradient(to_bottom,#80808008_1px,transparent_1px)] bg-[size:24px_24px]" />
<div className="absolute left-0 right-0 top-0 -z-10 m-auto h-[310px] w-[310px] rounded-full bg-neutral-400/20 opacity-20 blur-[100px]" />
<PaletteTrigger onOpen={() => setOpen(true)} />
<AnimatePresence>
{open && (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh] px-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setOpen(false)}
className="absolute inset-0 bg-neutral-950/20 backdrop-blur-[2px]"
/>
<AnimatePresence>
{notification.visible && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="absolute bottom-12 z-50 px-4 py-2 bg-neutral-900 dark:bg-white text-white dark:text-black rounded-full text-xs font-medium shadow-lg"
>
{notification.message}
</motion.div>
)}
</AnimatePresence>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-neutral-200 dark:border-white/10 bg-white dark:bg-neutral-900 shadow-2xl shadow-neutral-500/10 dark:shadow-black/50"
>
<Command
filter={(value, s) => {
if (mode === 'ai') return 1
if (value.toLowerCase().includes(s.toLowerCase())) return 1
return 0
}}
className="w-full bg-transparent"
loop
>
<div className="flex items-center border-b border-neutral-100 dark:border-white/5 px-4 py-4">
<AnimatePresence mode="wait">
{mode === 'ai' ? (
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5 }}
className="mr-3 flex items-center justify-center w-6 h-6 rounded-lg bg-black dark:bg-white"
>
<Bot className="w-3.5 h-3.5 text-white dark:text-black" />
</motion.div>
) : (
<motion.div initial={{ opacity: 0, scale: 0.5 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.5 }}>
<Search className="mr-3 h-5 w-5 text-neutral-400" />
</motion.div>
)}
</AnimatePresence>
<Command.Input
value={search}
onValueChange={setSearch}
placeholder={
mode === 'ai'
? 'Ask AI to generate code, explain concepts...'
: 'Type a command or search...'
}
className="flex-1 bg-transparent text-base outline-none placeholder:text-neutral-400 dark:text-white dark:placeholder:text-neutral-600"
autoFocus
/>
{mode === 'command' && (
<button
onClick={() => setMode('ai')}
className="px-2.5 py-1.5 text-[11px] font-medium bg-black/5 dark:bg-white/10 text-neutral-600 dark:text-neutral-300 rounded-md border border-black/5 dark:border-white/5 hover:bg-black/10 dark:hover:bg-white/20 transition-colors flex items-center gap-1.5"
>
<Wand2 className="w-3 h-3" />
Ask AI
</button>
)}
{mode === 'ai' && (
<button
onClick={() => {
setMode('command')
setSearch('')
setAiResponse('')
}}
className="px-2 py-1 text-[10px] font-medium bg-neutral-100 dark:bg-white/10 text-neutral-500 dark:text-neutral-400 rounded-md hover:bg-neutral-200 dark:hover:bg-white/20 transition-colors"
>
Esc
</button>
)}
</div>
<div className={cn('relative min-h-[300px] bg-neutral-50/30 dark:bg-black/20')}>
{mode === 'command' ? (
<CommandMode theme={theme} onSelect={handleCommandSelect} />
) : (
<AiMode
search={search}
aiResponse={aiResponse}
isTyping={isTyping}
onSearchChange={setSearch}
onCopy={() => showNotification('Code copied to clipboard!')}
onApply={() => showNotification('Changes applied successfully!')}
/>
)}
</div>
<PaletteFooter />
</Command>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
)
}
Source Code
"use client"
import { Bot, Wand2 } from "lucide-react"
import { motion } from "framer-motion"
const USER_IMAGE =
"https://i.postimg.cc/3xgQH76g/Whats-App-Image-2026-02-19-at-8-23-43-PM.jpg"
const SUGGESTIONS = ["How do I install this?", "Switch to dark mode", "Add new event type"]
type AiModeProps = {
search: string
aiResponse: string
isTyping: boolean
onSearchChange: (value: string) => void
onCopy: () => void
onApply: () => void
}
export function AiMode({ search, aiResponse, isTyping, onSearchChange, onCopy, onApply }: AiModeProps) {
return (
<div className="p-6 h-full flex flex-col">
{search.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-center text-neutral-500">
<div className="w-12 h-12 rounded-xl bg-black/5 dark:bg-white/5 flex items-center justify-center mb-4 border border-black/5 dark:border-white/5">
<Wand2 className="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
</div>
<h3 className="text-sm font-semibold text-neutral-900 dark:text-white mb-1">AI Assistant Ready</h3>
<p className="text-xs max-w-[250px] text-neutral-400">
Ask questions about your codebase, documentation, or generate code snippets.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-2">
{SUGGESTIONS.map((q) => (
<button
key={q}
onClick={() => onSearchChange(q)}
className="px-3 py-1.5 rounded-full border border-neutral-200 dark:border-white/10 text-xs text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-white/30 hover:text-neutral-900 dark:hover:text-white transition-all bg-white dark:bg-white/5 shadow-sm"
>
{q}
</button>
))}
</div>
</div>
) : (
<div className="flex-1 overflow-y-auto custom-scrollbar">
<div className="flex gap-4 mb-6">
<div className="w-8 h-8 rounded-full bg-neutral-100 dark:bg-white/10 border border-neutral-200 dark:border-white/5 flex-shrink-0 flex items-center justify-center overflow-hidden">
<img src={USER_IMAGE} alt="User" className="w-full h-full object-cover" />
</div>
<div className="flex-1 pt-1.5">
<p className="text-sm text-neutral-800 dark:text-neutral-200 font-medium">{search}</p>
</div>
</div>
{(aiResponse || isTyping) && (
<div className="flex gap-4">
<div className="w-8 h-8 rounded-full bg-black dark:bg-white flex-shrink-0 flex items-center justify-center shadow-lg shadow-black/10">
<Bot className="w-4 h-4 text-white dark:text-black" />
</div>
<div className="flex-1 pt-1.5">
<div className="text-sm text-neutral-600 dark:text-neutral-300 leading-relaxed">
{aiResponse}
{isTyping && (
<span className="inline-block w-1.5 h-4 ml-1 bg-neutral-400 animate-pulse align-middle" />
)}
</div>
{!isTyping && aiResponse && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="mt-4 flex gap-2">
<button
onClick={onCopy}
className="px-3 py-1.5 rounded-lg bg-white dark:bg-white/5 border border-neutral-200 dark:border-white/10 text-xs font-medium hover:bg-neutral-50 dark:hover:bg-white/10 transition-colors shadow-sm text-neutral-700 dark:text-neutral-300"
>
Copy Code
</button>
<button
onClick={onApply}
className="px-3 py-1.5 rounded-lg bg-black dark:bg-white text-white dark:text-black text-xs font-medium hover:opacity-90 transition-opacity shadow-sm"
>
Apply Changes
</button>
</motion.div>
)}
</div>
</div>
)}
</div>
)}
</div>
)
}
Source Code
"use client"
import { Command } from "cmdk"
import { Search } from "lucide-react"
import { COMMANDS } from "./commands"
type CommandModeProps = {
theme?: string
onSelect: (id: string) => void
}
export function CommandMode({ theme, onSelect }: CommandModeProps) {
return (
<Command.List className="max-h-[400px] overflow-y-auto p-2 scroll-py-2 custom-scrollbar">
<Command.Empty className="py-12 text-center text-sm text-neutral-400 flex flex-col items-center gap-2">
<Search className="w-8 h-8 opacity-20" />
No results found.
</Command.Empty>
{COMMANDS.map((group) => (
<Command.Group
key={group.heading}
heading={group.heading}
className="text-[10px] font-semibold text-neutral-400 uppercase tracking-wider mb-2 px-2 mt-2 first:mt-0"
>
{group.items.map((item) => (
<Command.Item
key={item.id}
value={item.label}
onSelect={() => onSelect(item.id)}
className="group flex items-center justify-between rounded-lg px-3 py-3 text-sm text-neutral-600 dark:text-neutral-300 aria-selected:bg-white dark:aria-selected:bg-neutral-800 aria-selected:text-black dark:aria-selected:text-white aria-selected:shadow-md aria-selected:scale-[1.01] cursor-pointer transition-all duration-200 mb-1 border border-transparent aria-selected:border-neutral-200/50 dark:aria-selected:border-white/10"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-neutral-100 dark:bg-white/5 border border-neutral-200 dark:border-white/5 group-aria-selected:bg-neutral-50 dark:group-aria-selected:bg-white/10 transition-colors">
<item.icon className="w-4 h-4 text-neutral-500 dark:text-neutral-400 group-aria-selected:text-neutral-900 dark:group-aria-selected:text-white" />
</div>
<span className="font-medium">{item.label}</span>
{item.id.startsWith("theme-") && theme === item.id.replace("theme-", "") && (
<span className="text-[9px] uppercase tracking-wider text-emerald-600 dark:text-emerald-400 font-semibold">
Active
</span>
)}
</div>
{item.shortcut && (
<span className="text-[10px] font-mono text-neutral-400 bg-neutral-100 dark:bg-white/5 px-1.5 py-0.5 rounded border border-neutral-200 dark:border-white/5 group-aria-selected:border-neutral-300 dark:group-aria-selected:border-white/10 transition-colors">
{item.shortcut}
</span>
)}
</Command.Item>
))}
</Command.Group>
))}
</Command.List>
)
}
commands.ts
import {
Bot,
FileText,
Sun,
Moon,
Monitor,
Settings,
LayoutDashboard,
Github,
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
export type CommandItem = {
id: string
icon: LucideIcon
label: string
shortcut?: string
}
export type CommandGroup = {
heading: string
items: CommandItem[]
}
export const COMMANDS: CommandGroup[] = [
{
heading: "Suggestions",
items: [
{ id: "ask-ai", icon: Bot, label: "Ask AI Assistant", shortcut: "A" },
{ id: "search-docs", icon: FileText, label: "Search Documentation", shortcut: "S" },
],
},
{
heading: "Appearance",
items: [
{ id: "theme-light", icon: Sun, label: "Light Mode", shortcut: "L" },
{ id: "theme-dark", icon: Moon, label: "Dark Mode", shortcut: "D" },
{ id: "theme-system", icon: Monitor, label: "System Theme", shortcut: "S" },
],
},
{
heading: "System",
items: [{ id: "settings", icon: Settings, label: "Open Settings", shortcut: "," }],
},
{
heading: "Navigation",
items: [
{ id: "home", icon: LayoutDashboard, label: "Go to Dashboard", shortcut: "G D" },
{ id: "projects", icon: Github, label: "Go to Projects", shortcut: "G P" },
],
},
]
Source Code
"use client"
import { ArrowUp, ArrowDown, CornerDownLeft, Zap } from "lucide-react"
export function PaletteFooter() {
return (
<div className="border-t border-neutral-100 dark:border-white/5 p-2.5 flex justify-between items-center bg-white dark:bg-neutral-900 text-[10px] text-neutral-400 font-medium">
<div className="flex gap-3">
<span className="flex items-center gap-1.5">
<ArrowUp className="w-3 h-3" />
<ArrowDown className="w-3 h-3" />
Navigate
</span>
<span className="flex items-center gap-1.5">
<CornerDownLeft className="w-3 h-3" />
Select
</span>
</div>
<div className="flex items-center gap-2 opacity-60">
<Zap className="w-3 h-3" />
Velocity AI Ready
</div>
</div>
)
}
Source Code
"use client"
import { motion } from "framer-motion"
import { Search, Command as CommandKey } from "lucide-react"
type PaletteTriggerProps = {
onOpen: () => void
}
export function PaletteTrigger({ onOpen }: PaletteTriggerProps) {
return (
<motion.button
whileHover={{ scale: 1.01, boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" }}
whileTap={{ scale: 0.99 }}
onClick={onOpen}
className="group relative flex items-center gap-3 rounded-xl bg-white dark:bg-neutral-900 px-8 py-4 text-left shadow-xl shadow-neutral-200/50 dark:shadow-black/50 border border-neutral-200 dark:border-white/10 transition-all hover:border-neutral-300 dark:hover:border-white/20"
>
<Search className="w-5 h-5 text-neutral-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" />
<span className="text-sm font-medium text-neutral-500 group-hover:text-neutral-800 dark:text-neutral-400 dark:group-hover:text-white transition-colors">
Search commands...
</span>
<kbd className="ml-12 pointer-events-none inline-flex h-6 select-none items-center gap-1 rounded-md border border-neutral-200 dark:border-white/10 bg-neutral-50 dark:bg-white/5 px-2 font-mono text-[11px] font-medium text-neutral-400 dark:text-neutral-500">
<CommandKey className="w-3 h-3" />K
</kbd>
</motion.button>
)
}
Dependencies
framer-motion: latestlucide-react: latestcmdk: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Whether the palette is visible. |
| onOpenChange | (open: boolean) => void | undefined | Callback when visibility toggles. |
| mode | 'command' | 'ai' | 'command' | Initial mode of the palette. |
| commands | Array<{ heading: string; items: Array<{ id: string; icon: React.ComponentType<any>; label: string; shortcut?: string }> }> | Built-in examples | Command groups to render. |
| className | string | undefined | Additional CSS classes. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.