{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-ai-search-modal",
  "title": "Search Modal",
  "description": "A full-featured chat search modal with conversation preview, inline rename, delete, keyboard navigation, and time-grouped results.",
  "dependencies": [
    "motion",
    "@hugeicons/react",
    "@hugeicons/core-free-icons",
    "react-markdown",
    "remark-gfm",
    "rehype-highlight"
  ],
  "registryDependencies": [
    "button",
    "@odysseyui/components-primitives-avatar"
  ],
  "files": [
    {
      "path": "registry/components/ai/search-modal/index.tsx",
      "content": "'use client';\n\nimport { useState, useMemo, useRef, useEffect } from 'react';\nimport { motion, AnimatePresence } from 'motion/react';\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport {\n  Search01Icon,\n  Add01Icon,\n  Delete02Icon,\n  PencilEdit01Icon,\n  UserIcon,\n} from '@hugeicons/core-free-icons';\nimport AgentAvatar, {\n  type AvatarColor,\n} from '@/components/odyssey/components/primitives/avatar';\nimport MarkdownRenderer from './markdown-renderer';\n\n/* ── types ───────────────────────────────────────────────── */\n\ninterface Message {\n  id: string;\n  role: 'user' | 'assistant';\n  content: string;\n  createdAt: number;\n}\n\ninterface Chat {\n  id: string;\n  title: string;\n  agentId: string;\n  modelId: string;\n  pinned: boolean;\n  folderId: string | null;\n  createdAt: number;\n  updatedAt: number;\n  messages: Message[];\n}\n\ninterface Agent {\n  id: string;\n  name: string;\n  description: string;\n  systemPrompt: string;\n  avatarColor: AvatarColor;\n  isDefault?: boolean;\n}\n\n/* ── dummy data ──────────────────────────────────────────── */\n\nconst now = Date.now();\nconst min = 60_000;\nconst hr = 3_600_000;\nconst day = 86_400_000;\n\nconst DUMMY_AGENTS: Agent[] = [\n  {\n    id: 'default',\n    name: 'Apollo',\n    description: 'General-purpose AI assistant.',\n    systemPrompt: '',\n    avatarColor: 'blue',\n    isDefault: true,\n  },\n  {\n    id: 'code-expert',\n    name: 'Cipher',\n    description: 'Expert software engineer.',\n    systemPrompt: '',\n    avatarColor: 'green',\n  },\n  {\n    id: 'creative-writer',\n    name: 'Muse',\n    description: 'Creative writing assistant.',\n    systemPrompt: '',\n    avatarColor: 'violet',\n  },\n];\n\nconst DUMMY_CHATS: Chat[] = [\n  {\n    id: 'chat-1',\n    title: 'Building a REST API with Node.js',\n    agentId: 'code-expert',\n    modelId: 'gpt-4o',\n    pinned: false,\n    folderId: null,\n    createdAt: now - 20 * min,\n    updatedAt: now - 20 * min,\n    messages: [\n      {\n        id: 'm1',\n        role: 'user',\n        content: 'How do I build a REST API with Node.js and Express?',\n        createdAt: now - 20 * min,\n      },\n      {\n        id: 'm2',\n        role: 'assistant',\n        content:\n          \"Here's a quick guide to building a REST API with Node.js and Express:\\n\\n```js\\nconst express = require('express')\\nconst app = express()\\n\\napp.use(express.json())\\n\\napp.get('/api/items', (req, res) => {\\n  res.json({ items: [] })\\n})\\n\\napp.listen(3000, () => console.log('Server running on port 3000'))\\n```\\n\\nKey steps:\\n1. Install Express: `npm install express`\\n2. Define routes for CRUD operations\\n3. Use middleware like `express.json()` for parsing request bodies\",\n        createdAt: now - 19 * min,\n      },\n    ],\n  },\n  {\n    id: 'chat-2',\n    title: 'Trip planning for Japan',\n    agentId: 'default',\n    modelId: 'gpt-4o',\n    pinned: false,\n    folderId: null,\n    createdAt: now - 2 * hr,\n    updatedAt: now - 2 * hr,\n    messages: [\n      {\n        id: 'm3',\n        role: 'user',\n        content: 'I want to plan a 2-week trip to Japan. Where should I start?',\n        createdAt: now - 2 * hr,\n      },\n      {\n        id: 'm4',\n        role: 'assistant',\n        content:\n          \"Great choice! Japan is an incredible destination. For a 2-week trip I'd suggest:\\n\\n- **Tokyo** (4-5 days): Shibuya, Shinjuku, Akihabara, Asakusa\\n- **Kyoto** (3-4 days): Fushimi Inari, Arashiyama, Gion\\n- **Osaka** (2 days): Dotonbori, street food, day trip to Nara\\n- **Hakone or Nikko** (1-2 days): Mt Fuji views, nature\\n\\nBook a **JR Pass** for bullet train travel between cities.\",\n        createdAt: now - 2 * hr + min,\n      },\n    ],\n  },\n  {\n    id: 'chat-3',\n    title: 'Short story: The Last Library',\n    agentId: 'creative-writer',\n    modelId: 'gpt-4o',\n    pinned: false,\n    folderId: null,\n    createdAt: now - 5 * hr,\n    updatedAt: now - 5 * hr,\n    messages: [\n      {\n        id: 'm5',\n        role: 'user',\n        content:\n          'Write the opening paragraph of a dystopian short story about the last library on Earth.',\n        createdAt: now - 5 * hr,\n      },\n      {\n        id: 'm6',\n        role: 'assistant',\n        content:\n          \"The last library smelled of time itself—dust and cedar and the faint, sweet rot of pages turning back into earth. Elara pressed her palm against the iron door, feeling the cold seep through her glove, and listened. Inside, ten thousand books held their breath. Outside, the Silence Bureau's drones swept low arcs across the grey sky, their sensors hungry for the one thing the State could never quite kill: a story.\",\n        createdAt: now - 5 * hr + min,\n      },\n    ],\n  },\n  {\n    id: 'chat-4',\n    title: 'Understanding React Server Components',\n    agentId: 'code-expert',\n    modelId: 'gpt-4o',\n    pinned: false,\n    folderId: null,\n    createdAt: now - day,\n    updatedAt: now - day,\n    messages: [\n      {\n        id: 'm7',\n        role: 'user',\n        content:\n          'Can you explain React Server Components and when to use them?',\n        createdAt: now - day,\n      },\n      {\n        id: 'm8',\n        role: 'assistant',\n        content:\n          \"React Server Components (RSCs) run **only on the server** and never ship JavaScript to the client. They're great for:\\n\\n- **Data fetching** directly from databases\\n- **Reducing bundle size** — no client JS for heavy components\\n- **Layouts and shells** that don't need interactivity\\n\\nUse `'use client'` at the top of any file that needs hooks, event handlers, or browser APIs. Everything else can stay as a Server Component by default in Next.js 13+.\",\n        createdAt: now - day + min,\n      },\n    ],\n  },\n  {\n    id: 'chat-5',\n    title: 'Meditation techniques for beginners',\n    agentId: 'default',\n    modelId: 'gpt-4o',\n    pinned: false,\n    folderId: null,\n    createdAt: now - 2 * day,\n    updatedAt: now - 2 * day,\n    messages: [\n      {\n        id: 'm9',\n        role: 'user',\n        content: 'What are some good meditation techniques for beginners?',\n        createdAt: now - 2 * day,\n      },\n    ],\n  },\n  {\n    id: 'chat-6',\n    title: 'CSS Grid vs Flexbox',\n    agentId: 'code-expert',\n    modelId: 'gpt-4o',\n    pinned: false,\n    folderId: null,\n    createdAt: now - 10 * day,\n    updatedAt: now - 10 * day,\n    messages: [\n      {\n        id: 'm10',\n        role: 'user',\n        content: 'When should I use CSS Grid instead of Flexbox?',\n        createdAt: now - 10 * day,\n      },\n      {\n        id: 'm11',\n        role: 'assistant',\n        content:\n          \"**Use Grid** when you need two-dimensional layout control (rows *and* columns), like page layouts, card grids, or dashboards.\\n\\n**Use Flexbox** when you're laying out items in a single direction — a row of buttons, a nav bar, or stacking elements vertically in a sidebar.\\n\\nA practical rule: if you're thinking in rows *and* columns → Grid. If you're thinking in one axis → Flexbox.\",\n        createdAt: now - 10 * day + min,\n      },\n    ],\n  },\n];\n\n/* ── helpers ─────────────────────────────────────────────── */\n\nfunction timeAgo(ts: number): string {\n  const diff = Date.now() - ts;\n  const mins = Math.floor(diff / 60000);\n  if (mins < 1) return 'just now';\n  if (mins < 60) return `${mins}m ago`;\n  const hrs = Math.floor(mins / 60);\n  if (hrs < 24) return `${hrs}h ago`;\n  const days = Math.floor(hrs / 24);\n  if (days === 1) return '1 day ago';\n  if (days < 7) return `${days}d ago`;\n  return new Date(ts).toLocaleDateString();\n}\n\nfunction groupSearchResults(chats: Chat[]) {\n  const n = new Date();\n  const todayStart = new Date(\n    n.getFullYear(),\n    n.getMonth(),\n    n.getDate(),\n  ).getTime();\n  const yesterdayStart = todayStart - 86_400_000;\n  const weekStart = todayStart - 7 * 86_400_000;\n\n  const groups: { label: string; chats: Chat[] }[] = [\n    { label: 'Today', chats: [] },\n    { label: 'Yesterday', chats: [] },\n    { label: 'Last 7 Days', chats: [] },\n    { label: 'Earlier', chats: [] },\n  ];\n  for (const c of chats) {\n    if (c.updatedAt >= todayStart) groups[0].chats.push(c);\n    else if (c.updatedAt >= yesterdayStart) groups[1].chats.push(c);\n    else if (c.updatedAt >= weekStart) groups[2].chats.push(c);\n    else groups[3].chats.push(c);\n  }\n  return groups.filter((g) => g.chats.length > 0);\n}\n\n/* ── search chat item ────────────────────────────────────── */\n\nfunction SearchChatItem({\n  chat,\n  isSelected,\n  isRenaming,\n  renameValue,\n  onRenameChange,\n  onRenameConfirm,\n  onRenameCancel,\n  onSelect,\n  onGo,\n  onEdit,\n  onDelete,\n}: {\n  chat: Chat;\n  isSelected: boolean;\n  isRenaming: boolean;\n  renameValue: string;\n  onRenameChange: (v: string) => void;\n  onRenameConfirm: () => void;\n  onRenameCancel: () => void;\n  onSelect: () => void;\n  onGo: () => void;\n  onEdit: () => void;\n  onDelete: () => void;\n}) {\n  const [hovered, setHovered] = useState(false);\n  const showActions = (hovered || isSelected) && !isRenaming;\n\n  return (\n    <div\n      onClick={onSelect}\n      onDoubleClick={onGo}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      className={cn(\n        'group relative flex h-9 cursor-pointer items-center rounded-xl px-2.5 text-sm transition-colors',\n        isSelected ? 'bg-muted/60' : 'hover:bg-muted/30',\n      )}\n    >\n      <div className=\"min-w-0 flex-1 pr-18\">\n        {isRenaming ? (\n          <input\n            autoFocus\n            value={renameValue}\n            onChange={(e) => onRenameChange(e.target.value)}\n            onBlur={onRenameConfirm}\n            onKeyDown={(e) => {\n              if (e.key === 'Enter') onRenameConfirm();\n              if (e.key === 'Escape') onRenameCancel();\n            }}\n            className=\"w-full bg-transparent text-sm text-foreground outline-none\"\n            onClick={(e) => e.stopPropagation()}\n          />\n        ) : (\n          <p className=\"truncate font-medium text-foreground/90\">\n            {chat.title}\n          </p>\n        )}\n      </div>\n\n      {!isRenaming && (\n        <div className=\"absolute top-0 right-1 flex h-full items-center\">\n          <AnimatePresence mode=\"wait\">\n            {showActions ? (\n              <motion.div\n                key=\"actions\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 0.1 }}\n                className=\"relative flex items-center gap-0.5 pl-6\"\n              >\n                <div\n                  className={cn(\n                    'absolute inset-0',\n                    isSelected ? 'from-muted/60' : 'from-muted/30',\n                  )}\n                />\n                <div className=\"relative flex items-center gap-0.5\">\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      onEdit();\n                    }}\n                    className=\"text-muted-foreground hover:text-foreground\"\n                    title=\"Rename\"\n                  >\n                    <HugeiconsIcon\n                      icon={PencilEdit01Icon}\n                      className=\"size-3.5\"\n                      strokeWidth={2}\n                    />\n                  </Button>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      onDelete();\n                    }}\n                    className=\"text-muted-foreground hover:text-destructive\"\n                    title=\"Delete\"\n                  >\n                    <HugeiconsIcon\n                      icon={Delete02Icon}\n                      className=\"size-3.5\"\n                      strokeWidth={2}\n                    />\n                  </Button>\n                </div>\n              </motion.div>\n            ) : (\n              <motion.span\n                key=\"time\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ duration: 0.1 }}\n                className=\"pr-1 text-[10px] text-muted-foreground/50\"\n              >\n                {timeAgo(chat.updatedAt)}\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </div>\n      )}\n    </div>\n  );\n}\n\n/* ── search modal ────────────────────────────────────────── */\n\nexport default function SearchModal({\n  open,\n  onClose,\n  onNewChat,\n}: {\n  open: boolean;\n  onClose: () => void;\n  onNewChat?: () => void;\n}) {\n  const [chats, setChats] = useState<Chat[]>(DUMMY_CHATS);\n  const [query, setQuery] = useState('');\n  const [selectedId, setSelectedId] = useState<string | null>(null);\n  const [showPreview, setShowPreview] = useState(true);\n  const [renamingId, setRenamingId] = useState<string | null>(null);\n  const [renameValue, setRenameValue] = useState('');\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const results = useMemo(() => {\n    if (!query.trim()) return chats;\n    const q = query.toLowerCase();\n    return chats.filter(\n      (c) =>\n        c.title.toLowerCase().includes(q) ||\n        c.messages.some((m) => m.content.toLowerCase().includes(q)),\n    );\n  }, [chats, query]);\n\n  const grouped = useMemo(() => groupSearchResults(results), [results]);\n  const flatList = useMemo(() => grouped.flatMap((g) => g.chats), [grouped]);\n\n  const selectedChat = useMemo(\n    () => chats.find((c) => c.id === selectedId) ?? null,\n    [chats, selectedId],\n  );\n\n  const selectedChatAgent = useMemo(() => {\n    if (!selectedChat) return null;\n    return (\n      DUMMY_AGENTS.find((a) => a.id === selectedChat.agentId) ?? DUMMY_AGENTS[0]\n    );\n  }, [selectedChat]);\n\n  // Auto-select first result\n  useEffect(() => {\n    if (flatList.length > 0 && !flatList.find((c) => c.id === selectedId)) {\n      setSelectedId(flatList[0].id);\n    }\n  }, [flatList, selectedId]);\n\n  // Reset on open\n  useEffect(() => {\n    if (open) {\n      setQuery('');\n      setSelectedId(null);\n      setRenamingId(null);\n      setTimeout(() => inputRef.current?.focus(), 50);\n    }\n  }, [open]);\n\n  // Keyboard navigation\n  useEffect(() => {\n    if (!open) return;\n    const handler = (e: KeyboardEvent) => {\n      if (e.key === 'Escape' && !renamingId) {\n        onClose();\n        return;\n      }\n      if (e.key === 'ArrowDown') {\n        e.preventDefault();\n        const idx = flatList.findIndex((c) => c.id === selectedId);\n        if (idx < flatList.length - 1) setSelectedId(flatList[idx + 1].id);\n      }\n      if (e.key === 'ArrowUp') {\n        e.preventDefault();\n        const idx = flatList.findIndex((c) => c.id === selectedId);\n        if (idx > 0) setSelectedId(flatList[idx - 1].id);\n      }\n      if (e.key === 'Enter' && selectedId && !renamingId) {\n        e.preventDefault();\n        onClose();\n      }\n      if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'P') {\n        e.preventDefault();\n        setShowPreview((v) => !v);\n      }\n      if (\n        (e.ctrlKey || e.metaKey) &&\n        e.shiftKey &&\n        e.key === 'E' &&\n        selectedId\n      ) {\n        e.preventDefault();\n        const chat = chats.find((c) => c.id === selectedId);\n        if (chat) {\n          setRenamingId(selectedId);\n          setRenameValue(chat.title);\n        }\n      }\n      if (\n        (e.ctrlKey || e.metaKey) &&\n        e.shiftKey &&\n        e.key === 'D' &&\n        selectedId\n      ) {\n        e.preventDefault();\n        setChats((prev) => prev.filter((c) => c.id !== selectedId));\n        setSelectedId(null);\n      }\n    };\n    document.addEventListener('keydown', handler);\n    return () => document.removeEventListener('keydown', handler);\n  }, [open, selectedId, flatList, renamingId, chats, onClose]);\n\n  const handleRenameConfirm = () => {\n    if (renamingId && renameValue.trim()) {\n      setChats((prev) =>\n        prev.map((c) =>\n          c.id === renamingId ? { ...c, title: renameValue.trim() } : c,\n        ),\n      );\n    }\n    setRenamingId(null);\n  };\n\n  return (\n    <AnimatePresence>\n      {open && (\n        <>\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm\"\n            onClick={onClose}\n          />\n          <motion.div\n            initial={{ opacity: 0, scale: 0.96, y: -10 }}\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            exit={{ opacity: 0, scale: 0.96, y: -10 }}\n            transition={{ duration: 0.2, ease: 'easeOut' }}\n            className=\"fixed inset-4 z-50 m-auto flex max-h-[80dvh] min-h-180 max-w-5xl items-center justify-center sm:inset-x-0 sm:inset-y-auto sm:top-[10%] sm:px-4\"\n          >\n            <div className=\"flex max-h-[80dvh] min-h-180 w-full flex-col overflow-hidden rounded-2xl border border-border/50 bg-popover shadow-2xl\">\n              {/* Search input */}\n              <div className=\"flex items-center gap-3 border-b border-border/50 px-4 py-3\">\n                <HugeiconsIcon\n                  icon={Search01Icon}\n                  className=\"size-5 shrink-0 text-muted-foreground\"\n                  strokeWidth={2}\n                />\n                <input\n                  ref={inputRef}\n                  autoFocus\n                  value={query}\n                  onChange={(e) => setQuery(e.target.value)}\n                  placeholder=\"Search chats...\"\n                  className=\"flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/60\"\n                />\n                <kbd className=\"rounded-md border border-border/50 bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground\">\n                  ESC\n                </kbd>\n              </div>\n\n              {/* Content */}\n              <div className=\"flex min-h-0 flex-1\">\n                {/* Left: list */}\n                <div\n                  className={cn(\n                    'flex flex-col',\n                    showPreview\n                      ? 'w-full sm:w-2/5 sm:border-r sm:border-border/50'\n                      : 'w-full',\n                  )}\n                >\n                  {/* Actions */}\n                  <div className=\"border-b border-border/30 p-2\">\n                    <p className=\"mb-1 px-2 text-[10px] font-semibold tracking-wider text-muted-foreground/60 uppercase\">\n                      Actions\n                    </p>\n                    <button\n                      onClick={() => {\n                        onNewChat?.();\n                        onClose();\n                      }}\n                      className=\"flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm text-foreground/90 transition-colors hover:bg-muted/50\"\n                    >\n                      <HugeiconsIcon\n                        icon={Add01Icon}\n                        className=\"size-4 text-muted-foreground\"\n                        strokeWidth={2}\n                      />\n                      New Chat\n                    </button>\n                  </div>\n\n                  {/* Chat list */}\n                  <div className=\"scrollbar-thin flex-1 overflow-y-auto p-2\">\n                    {grouped.length === 0 ? (\n                      <p className=\"py-8 text-center text-sm text-muted-foreground\">\n                        No conversations found\n                      </p>\n                    ) : (\n                      grouped.map((group) => (\n                        <div key={group.label} className=\"mb-2\">\n                          <p className=\"px-2 py-1.5 text-[10px] font-semibold tracking-wider text-muted-foreground/60 uppercase\">\n                            {group.label}\n                          </p>\n                          {group.chats.map((chat) => (\n                            <SearchChatItem\n                              key={chat.id}\n                              chat={chat}\n                              isSelected={chat.id === selectedId}\n                              isRenaming={chat.id === renamingId}\n                              renameValue={renameValue}\n                              onRenameChange={setRenameValue}\n                              onRenameConfirm={handleRenameConfirm}\n                              onRenameCancel={() => setRenamingId(null)}\n                              onSelect={() => setSelectedId(chat.id)}\n                              onGo={() => onClose()}\n                              onEdit={() => {\n                                setRenamingId(chat.id);\n                                setRenameValue(chat.title);\n                              }}\n                              onDelete={() => {\n                                setChats((prev) =>\n                                  prev.filter((c) => c.id !== chat.id),\n                                );\n                                if (selectedId === chat.id) setSelectedId(null);\n                              }}\n                            />\n                          ))}\n                        </div>\n                      ))\n                    )}\n                  </div>\n                </div>\n\n                {/* Right: preview */}\n                {showPreview && (\n                  <div className=\"hidden flex-1 overflow-hidden sm:block\">\n                    {selectedChat && selectedChat.messages.length > 0 ? (\n                      <div className=\"scrollbar-thin h-full overflow-y-auto p-4\">\n                        <div className=\"space-y-4\">\n                          {selectedChat.messages.map((msg) => (\n                            <div key={msg.id} className=\"flex gap-2.5\">\n                              <div className=\"mt-0.5 shrink-0\">\n                                {msg.role === 'user' ? (\n                                  <div className=\"flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary\">\n                                    <HugeiconsIcon\n                                      icon={UserIcon}\n                                      className=\"size-3\"\n                                      strokeWidth={2}\n                                    />\n                                  </div>\n                                ) : (\n                                  <div className=\"size-6 overflow-hidden rounded-lg\">\n                                    <AgentAvatar\n                                      color={\n                                        selectedChatAgent?.avatarColor ?? 'blue'\n                                      }\n                                      size=\"md\"\n                                      className=\"origin-top-left scale-[0.5]\"\n                                    />\n                                  </div>\n                                )}\n                              </div>\n                              <div className=\"min-w-0 flex-1\">\n                                <p className=\"mb-1 text-[10px] font-medium text-sm text-muted-foreground\">\n                                  {msg.role === 'user'\n                                    ? 'You'\n                                    : (selectedChatAgent?.name ?? 'Assistant')}\n                                </p>\n                                {msg.role === 'assistant' ? (\n                                  <div className=\"text-sm [&_.markdown-body]:text-sm [&_.markdown-body_p]:mb-2 [&_.markdown-body_pre]:my-2\">\n                                    <MarkdownRenderer content={msg.content} />\n                                  </div>\n                                ) : (\n                                  <p className=\"text-sm leading-relaxed text-foreground/90\">\n                                    {msg.content}\n                                  </p>\n                                )}\n                              </div>\n                            </div>\n                          ))}\n                        </div>\n                      </div>\n                    ) : (\n                      <div className=\"flex h-full items-center justify-center\">\n                        <p className=\"text-sm text-muted-foreground/50\">\n                          {selectedChat\n                            ? 'No messages yet'\n                            : 'Select a conversation to preview'}\n                        </p>\n                      </div>\n                    )}\n                  </div>\n                )}\n              </div>\n\n              {/* Bottom actions */}\n              <div className=\"flex items-center gap-1 overflow-x-auto border-t border-border/50 px-3 py-2 sm:gap-2 sm:px-4 sm:py-3\">\n                <button\n                  onClick={() => setShowPreview((v) => !v)}\n                  className=\"hidden shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground sm:flex\"\n                >\n                  <span className=\"text-[10px]\">↕</span>\n                  {showPreview ? 'Hide' : 'Show'} Preview\n                  <kbd className=\"ml-1 rounded border border-border/50 bg-muted px-1 py-px text-[9px]\">\n                    Ctrl + P\n                  </kbd>\n                </button>\n                <div className=\"flex-1\" />\n                <button\n                  onClick={() => {\n                    if (selectedId) onClose();\n                  }}\n                  className=\"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs text-foreground/80 transition-colors hover:bg-muted/50\"\n                >\n                  Go\n                  <kbd className=\"hidden rounded border border-border/50 bg-muted px-1 py-px text-[9px] text-muted-foreground sm:inline\">\n                    ↵\n                  </kbd>\n                </button>\n                <button\n                  onClick={() => {\n                    if (selectedId) {\n                      const chat = chats.find((c) => c.id === selectedId);\n                      if (chat) {\n                        setRenamingId(selectedId);\n                        setRenameValue(chat.title);\n                      }\n                    }\n                  }}\n                  className=\"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs text-foreground/80 transition-colors hover:bg-muted/50\"\n                >\n                  Edit\n                  <kbd className=\"hidden rounded border border-border/50 bg-muted px-1 py-px text-[9px] text-muted-foreground sm:inline\">\n                    Ctrl + Shift + E\n                  </kbd>\n                </button>\n                <button\n                  onClick={() => {\n                    if (selectedId) {\n                      setChats((prev) =>\n                        prev.filter((c) => c.id !== selectedId),\n                      );\n                      setSelectedId(null);\n                    }\n                  }}\n                  className=\"flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs text-destructive/80 transition-colors hover:bg-destructive/10\"\n                >\n                  Delete\n                  <kbd className=\"hidden rounded border border-border/50 bg-muted px-1 py-px text-[9px] text-muted-foreground sm:inline\">\n                    Ctrl + Shift + D\n                  </kbd>\n                </button>\n              </div>\n            </div>\n          </motion.div>\n        </>\n      )}\n    </AnimatePresence>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/odysseyui/search-modal.tsx"
    },
    {
      "path": "registry/components/ai/search-modal/markdown-renderer.tsx",
      "content": "'use client';\n\nimport { memo, useState, useCallback } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport rehypeHighlight from 'rehype-highlight';\nimport { cn } from '@/lib/utils';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Copy01Icon, Tick01Icon } from '@hugeicons/core-free-icons';\n\nfunction CopyButton({ text }: { text: string }) {\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = useCallback(() => {\n    navigator.clipboard.writeText(text);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  }, [text]);\n\n  return (\n    <button\n      onClick={handleCopy}\n      className=\"flex items-center gap-1 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground\"\n    >\n      <HugeiconsIcon\n        icon={copied ? Tick01Icon : Copy01Icon}\n        className=\"size-3.5\"\n        strokeWidth={1.5}\n      />\n      {copied ? 'Copied' : 'Copy'}\n    </button>\n  );\n}\n\nconst MarkdownRenderer = memo(function MarkdownRenderer({\n  content,\n  className,\n}: {\n  content: string;\n  className?: string;\n}) {\n  return (\n    <div className={cn('markdown-body', className)}>\n      <ReactMarkdown\n        remarkPlugins={[remarkGfm]}\n        rehypePlugins={[rehypeHighlight]}\n        components={{\n          pre({ children, ...props }) {\n            const codeEl = (children as React.ReactElement[])?.[0] as\n              | React.ReactElement<{\n                  children?: React.ReactNode;\n                  className?: string;\n                }>\n              | undefined;\n            const codeText = codeEl?.props?.children\n              ? String(codeEl.props.children).replace(/\\n$/, '')\n              : '';\n            const langClass = codeEl?.props?.className ?? '';\n            const lang = langClass.replace(/^language-/, '');\n\n            return (\n              <div className=\"group/code relative my-3 overflow-hidden rounded-xl border border-border/40 bg-background\">\n                <div className=\"flex items-center justify-between rounded-t-xl border border-border/30 bg-muted px-4 py-2\">\n                  <span className=\"text-xs font-medium text-muted-foreground\">\n                    {lang || 'Code'}\n                  </span>\n                  <CopyButton text={codeText} />\n                </div>\n                <pre\n                  className=\"overflow-x-auto p-4 text-sm leading-relaxed\"\n                  {...props}\n                >\n                  {children}\n                </pre>\n              </div>\n            );\n          },\n          code({ className, children, ...props }) {\n            const isInline = !className;\n            if (isInline) {\n              return (\n                <code\n                  className=\"rounded-md bg-muted/60 px-1.5 py-0.5 font-mono text-[0.85em] text-foreground/90\"\n                  {...props}\n                >\n                  {children}\n                </code>\n              );\n            }\n            return (\n              <code className={className} {...props}>\n                {children}\n              </code>\n            );\n          },\n          p({ children, ...props }) {\n            return (\n              <p className=\"mb-3 leading-relaxed last:mb-0\" {...props}>\n                {children}\n              </p>\n            );\n          },\n          ul({ children, ...props }) {\n            return (\n              <ul className=\"mb-3 list-disc space-y-1 pl-6\" {...props}>\n                {children}\n              </ul>\n            );\n          },\n          ol({ children, ...props }) {\n            return (\n              <ol className=\"mb-3 list-decimal space-y-1 pl-6\" {...props}>\n                {children}\n              </ol>\n            );\n          },\n          h1({ children, ...props }) {\n            return (\n              <h1 className=\"mt-6 mb-3 text-xl font-bold first:mt-0\" {...props}>\n                {children}\n              </h1>\n            );\n          },\n          h2({ children, ...props }) {\n            return (\n              <h2 className=\"mt-5 mb-2 text-lg font-bold first:mt-0\" {...props}>\n                {children}\n              </h2>\n            );\n          },\n          h3({ children, ...props }) {\n            return (\n              <h3\n                className=\"mt-4 mb-2 text-base font-semibold first:mt-0\"\n                {...props}\n              >\n                {children}\n              </h3>\n            );\n          },\n          blockquote({ children, ...props }) {\n            return (\n              <blockquote\n                className=\"my-3 border-l-2 border-primary/40 pl-4 text-muted-foreground italic\"\n                {...props}\n              >\n                {children}\n              </blockquote>\n            );\n          },\n          table({ children, ...props }) {\n            return (\n              <div className=\"my-3 overflow-x-auto rounded-lg border border-border/40\">\n                <table className=\"w-full text-sm\" {...props}>\n                  {children}\n                </table>\n              </div>\n            );\n          },\n          th({ children, ...props }) {\n            return (\n              <th\n                className=\"border-b border-border/40 bg-muted/30 px-3 py-2 text-left font-semibold\"\n                {...props}\n              >\n                {children}\n              </th>\n            );\n          },\n          td({ children, ...props }) {\n            return (\n              <td className=\"border-b border-border/20 px-3 py-2\" {...props}>\n                {children}\n              </td>\n            );\n          },\n          a({ children, href, ...props }) {\n            return (\n              <a\n                href={href}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                className=\"text-primary underline decoration-primary/30 underline-offset-2 transition-colors hover:decoration-primary/60\"\n                {...props}\n              >\n                {children}\n              </a>\n            );\n          },\n          hr() {\n            return <hr className=\"my-4 border-border/30\" />;\n          },\n        }}\n      >\n        {content}\n      </ReactMarkdown>\n    </div>\n  );\n});\n\nexport default MarkdownRenderer;\n",
      "type": "registry:ui",
      "target": "components/odysseyui/markdown-renderer.tsx"
    }
  ],
  "type": "registry:ui"
}