{
  "name": "action-search-bar",
  "type": "registry:component",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "input"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @kokonutui\n * @description: A modern search bar component with action buttons and suggestions\n * @version: 1.0.0\n * @date: 2025-06-26\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport {\n  AudioLines,\n  BarChart2,\n  LayoutGrid,\n  PlaneTakeoff,\n  Search,\n  Send,\n  Video,\n} from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport useDebounce from \"@/hooks/use-debounce\";\n\ninterface Action {\n  id: string;\n  label: string;\n  icon: React.ReactNode;\n  description?: string;\n  short?: string;\n  end?: string;\n}\n\ninterface SearchResult {\n  actions: Action[];\n}\n\nconst ANIMATION_VARIANTS = {\n  container: {\n    hidden: { opacity: 0, height: 0 },\n    show: {\n      opacity: 1,\n      height: \"auto\",\n      transition: {\n        height: { duration: 0.4 },\n        staggerChildren: 0.1,\n      },\n    },\n    exit: {\n      opacity: 0,\n      height: 0,\n      transition: {\n        height: { duration: 0.3 },\n        opacity: { duration: 0.2 },\n      },\n    },\n  },\n  item: {\n    hidden: { opacity: 0, y: 20 },\n    show: {\n      opacity: 1,\n      y: 0,\n      transition: { duration: 0.3 },\n    },\n    exit: {\n      opacity: 0,\n      y: -10,\n      transition: { duration: 0.2 },\n    },\n  },\n} as const;\n\nconst allActionsSample = [\n  {\n    id: \"1\",\n    label: \"Book tickets\",\n    icon: <PlaneTakeoff className=\"h-4 w-4 text-blue-500\" />,\n    description: \"Operator\",\n    short: \"⌘K\",\n    end: \"Agent\",\n  },\n  {\n    id: \"2\",\n    label: \"Summarize\",\n    icon: <BarChart2 className=\"h-4 w-4 text-orange-500\" />,\n    description: \"gpt-5\",\n    short: \"⌘cmd+p\",\n    end: \"Command\",\n  },\n  {\n    id: \"3\",\n    label: \"Screen Studio\",\n    icon: <Video className=\"h-4 w-4 text-purple-500\" />,\n    description: \"Claude 4.1\",\n    short: \"\",\n    end: \"Application\",\n  },\n  {\n    id: \"4\",\n    label: \"Talk to Jarvis\",\n    icon: <AudioLines className=\"h-4 w-4 text-green-500\" />,\n    description: \"gpt-5 voice\",\n    short: \"\",\n    end: \"Active\",\n  },\n  {\n    id: \"5\",\n    label: \"Kokonut UI - Pro\",\n    icon: <LayoutGrid className=\"h-4 w-4 text-blue-500\" />,\n    description: \"Components\",\n    short: \"\",\n    end: \"Link\",\n  },\n];\n\nfunction ActionSearchBar({\n  actions = allActionsSample,\n  defaultOpen = false,\n}: {\n  actions?: Action[];\n  defaultOpen?: boolean;\n}) {\n  const [query, setQuery] = useState(\"\");\n  const [result, setResult] = useState<SearchResult | null>(null);\n  const [isFocused, setIsFocused] = useState(defaultOpen);\n  const [isTyping, setIsTyping] = useState(false);\n  const [selectedAction, setSelectedAction] = useState<Action | null>(null);\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const debouncedQuery = useDebounce(query, 200);\n\n  const filteredActions = useMemo(() => {\n    if (!debouncedQuery) return actions;\n\n    const normalizedQuery = debouncedQuery.toLowerCase().trim();\n    return actions.filter((action) => {\n      const searchableText =\n        `${action.label} ${action.description || \"\"}`.toLowerCase();\n      return searchableText.includes(normalizedQuery);\n    });\n  }, [debouncedQuery, actions]);\n\n  useEffect(() => {\n    if (!isFocused) {\n      setResult(null);\n      setActiveIndex(-1);\n      return;\n    }\n\n    setResult({ actions: filteredActions });\n    setActiveIndex(-1);\n  }, [filteredActions, isFocused]);\n\n  const handleInputChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      setQuery(e.target.value);\n      setIsTyping(true);\n      setActiveIndex(-1);\n    },\n    []\n  );\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (!result?.actions.length) return;\n\n      switch (e.key) {\n        case \"ArrowDown\":\n          e.preventDefault();\n          setActiveIndex((prev) =>\n            prev < result.actions.length - 1 ? prev + 1 : 0\n          );\n          break;\n        case \"ArrowUp\":\n          e.preventDefault();\n          setActiveIndex((prev) =>\n            prev > 0 ? prev - 1 : result.actions.length - 1\n          );\n          break;\n        case \"Enter\":\n          e.preventDefault();\n          if (activeIndex >= 0 && result.actions[activeIndex]) {\n            setSelectedAction(result.actions[activeIndex]);\n          }\n          break;\n        case \"Escape\":\n          setIsFocused(false);\n          setActiveIndex(-1);\n          break;\n      }\n    },\n    [result?.actions, activeIndex]\n  );\n\n  const handleActionClick = useCallback((action: Action) => {\n    setSelectedAction(action);\n  }, []);\n\n  const handleFocus = useCallback(() => {\n    setSelectedAction(null);\n    setIsFocused(true);\n    setActiveIndex(-1);\n  }, []);\n\n  const handleBlur = useCallback(() => {\n    setTimeout(() => {\n      setIsFocused(false);\n      setActiveIndex(-1);\n    }, 200);\n  }, []);\n\n  return (\n    <div className=\"mx-auto w-full max-w-xl\">\n      <div className=\"relative flex min-h-[300px] flex-col items-center justify-start\">\n        <div className=\"sticky top-0 z-10 w-full max-w-sm bg-background pt-4 pb-1\">\n          <label\n            className=\"mb-1 block font-medium text-gray-500 text-xs dark:text-gray-400\"\n            htmlFor=\"search\"\n          >\n            Search Commands\n          </label>\n          <div className=\"relative\">\n            <Input\n              aria-activedescendant={\n                activeIndex >= 0\n                  ? `action-${result?.actions[activeIndex]?.id}`\n                  : undefined\n              }\n              aria-autocomplete=\"list\"\n              aria-expanded={isFocused && !!result}\n              autoComplete=\"off\"\n              className=\"h-9 rounded-lg py-1.5 pr-9 pl-3 text-sm focus-visible:ring-offset-0\"\n              id=\"search\"\n              onBlur={handleBlur}\n              onChange={handleInputChange}\n              onFocus={handleFocus}\n              onKeyDown={handleKeyDown}\n              placeholder=\"What's up?\"\n              role=\"combobox\"\n              type=\"text\"\n              value={query}\n            />\n            <div className=\"absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2\">\n              <AnimatePresence mode=\"popLayout\">\n                {query.length > 0 ? (\n                  <motion.div\n                    animate={{ y: 0, opacity: 1 }}\n                    exit={{ y: 20, opacity: 0 }}\n                    initial={{ y: -20, opacity: 0 }}\n                    key=\"send\"\n                    transition={{ duration: 0.2 }}\n                  >\n                    <Send className=\"h-4 w-4 text-gray-400 dark:text-gray-500\" />\n                  </motion.div>\n                ) : (\n                  <motion.div\n                    animate={{ y: 0, opacity: 1 }}\n                    exit={{ y: 20, opacity: 0 }}\n                    initial={{ y: -20, opacity: 0 }}\n                    key=\"search\"\n                    transition={{ duration: 0.2 }}\n                  >\n                    <Search className=\"h-4 w-4 text-gray-400 dark:text-gray-500\" />\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"w-full max-w-sm\">\n          <AnimatePresence>\n            {isFocused && result && !selectedAction && (\n              <motion.div\n                animate=\"show\"\n                aria-label=\"Search results\"\n                className=\"mt-1 w-full overflow-hidden rounded-md border bg-white shadow-xs dark:border-gray-800 dark:bg-black\"\n                exit=\"exit\"\n                initial=\"hidden\"\n                role=\"listbox\"\n                variants={ANIMATION_VARIANTS.container}\n              >\n                <motion.ul role=\"none\">\n                  {result.actions.map((action) => (\n                    <motion.li\n                      aria-selected={\n                        activeIndex === result.actions.indexOf(action)\n                      }\n                      className={`flex cursor-pointer items-center justify-between rounded-md px-3 py-2 hover:bg-gray-200 dark:hover:bg-zinc-900 ${\n                        activeIndex === result.actions.indexOf(action)\n                          ? \"bg-gray-100 dark:bg-zinc-800\"\n                          : \"\"\n                      }`}\n                      id={`action-${action.id}`}\n                      key={action.id}\n                      layout\n                      onClick={() => handleActionClick(action)}\n                      role=\"option\"\n                      variants={ANIMATION_VARIANTS.item}\n                    >\n                      <div className=\"flex items-center justify-between gap-2\">\n                        <div className=\"flex items-center gap-2\">\n                          <span aria-hidden=\"true\" className=\"text-gray-500\">\n                            {action.icon}\n                          </span>\n                          <span className=\"font-medium text-gray-900 text-sm dark:text-gray-100\">\n                            {action.label}\n                          </span>\n                          {action.description && (\n                            <span className=\"text-gray-400 text-xs\">\n                              {action.description}\n                            </span>\n                          )}\n                        </div>\n                      </div>\n                      <div className=\"flex items-center gap-2\">\n                        {action.short && (\n                          <span\n                            aria-label={`Keyboard shortcut: ${action.short}`}\n                            className=\"text-gray-400 text-xs\"\n                          >\n                            {action.short}\n                          </span>\n                        )}\n                        {action.end && (\n                          <span className=\"text-right text-gray-400 text-xs\">\n                            {action.end}\n                          </span>\n                        )}\n                      </div>\n                    </motion.li>\n                  ))}\n                </motion.ul>\n                <div className=\"mt-2 border-gray-100 border-t px-3 py-2 dark:border-gray-800\">\n                  <div className=\"flex items-center justify-between text-gray-500 text-xs\">\n                    <span>Press ⌘K to open commands</span>\n                    <span>ESC to cancel</span>\n                  </div>\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default ActionSearchBar;\n",
      "path": "/components/kokonutui/action-search-bar.tsx",
      "target": "components/kokonutui/action-search-bar.tsx"
    },
    {
      "type": "registry:hook",
      "content": "import { useEffect, useState } from \"react\";\n\nfunction useDebounce<T>(value: T, delay = 500): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(timer);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n\nexport default useDebounce;\n",
      "path": "/hooks/use-debounce.ts",
      "target": "hooks/use-debounce.ts"
    }
  ]
}