Menu

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:

terminal
npx -y vui-registry-cli-v1@latest add ai-command-palette

Source Code

ai-command-palette.tsx
'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

ai-mode.tsx
"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

command-mode.tsx
"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

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

palette-footer.tsx
"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

palette-trigger.tsx
"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: latest
  • lucide-react: latest
  • cmdk: latest

Props

Component property reference.

NameTypeDefaultDescription
openbooleanfalseWhether the palette is visible.
onOpenChange(open: boolean) => voidundefinedCallback when visibility toggles.
mode'command' | 'ai''command'Initial mode of the palette.
commandsArray<{ heading: string; items: Array<{ id: string; icon: React.ComponentType<any>; label: string; shortcut?: string }> }>Built-in examplesCommand groups to render.
classNamestringundefinedAdditional CSS classes.
Context Worth Keeping In Orbit

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.