{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "favicon-search",
  "title": "Favicon Search",
  "description": "A search input that fetches and smoothly animates the site favicon into the search bar as you type a URL.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/favicon-search/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { Globe, Search, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction extractDomain(input: string): string | null {\n  if (!input.trim()) return null;\n  try {\n    const raw = input.includes(\"://\") ? input : `https://${input}`;\n    const url = new URL(raw);\n    const host = url.hostname.replace(/^www\\./, \"\");\n    if (host.includes(\".\") && host.split(\".\").every(Boolean)) return host;\n    return null;\n  } catch {\n    const cleaned = input.trim().replace(/^www\\./, \"\");\n    if (cleaned.includes(\".\") && cleaned.split(\".\").every(Boolean))\n      return cleaned;\n    return null;\n  }\n}\n\nfunction getFaviconUrl(domain: string, size = 64): string {\n  return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=${size}`;\n}\n\nexport interface FaviconSearchProps {\n  /** Controlled value */\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  onSearch?: (value: string, domain: string | null) => void;\n  placeholder?: string;\n  clearable?: boolean;\n  /** @default 64 */\n  faviconSize?: 16 | 32 | 64 | 128;\n  /** @default 350 */\n  debounce?: number;\n  className?: string;\n  inputClassName?: string;\n}\n\nconst FaviconSearch = React.forwardRef<HTMLInputElement, FaviconSearchProps>(\n  (\n    {\n      value: controlledValue,\n      defaultValue = \"\",\n      onChange,\n      onSearch,\n      placeholder = \"Enter a website URL…\",\n      clearable = true,\n      faviconSize = 64,\n      debounce = 350,\n      className,\n      inputClassName,\n    },\n    ref,\n  ) => {\n    const isControlled = controlledValue !== undefined;\n    const [internalValue, setInternalValue] = React.useState(defaultValue);\n    const value = isControlled ? controlledValue : internalValue;\n\n    const [domain, setDomain] = React.useState<string | null>(null);\n    const [faviconReady, setFaviconReady] = React.useState(false);\n    const [faviconError, setFaviconError] = React.useState(false);\n    const prevDomainRef = React.useRef<string | null>(null);\n\n    React.useEffect(() => {\n      const id = setTimeout(() => {\n        const d = extractDomain(value);\n        if (d !== prevDomainRef.current) {\n          prevDomainRef.current = d;\n          setFaviconReady(false);\n          setFaviconError(false);\n          setDomain(d);\n        }\n      }, debounce);\n      return () => clearTimeout(id);\n    }, [value, debounce]);\n\n    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      const v = e.target.value;\n      if (!isControlled) setInternalValue(v);\n      onChange?.(v);\n    };\n\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        onSearch?.(value, domain);\n      }\n    };\n\n    const handleClear = () => {\n      if (!isControlled) setInternalValue(\"\");\n      onChange?.(\"\");\n      setDomain(null);\n      setFaviconReady(false);\n      setFaviconError(false);\n      prevDomainRef.current = null;\n    };\n\n    const showFavicon = domain && faviconReady && !faviconError;\n\n    return (\n      <div\n        className={cn(\n          \"relative flex items-center w-full max-w-md group\",\n          className,\n        )}\n      >\n        <div className=\"pointer-events-none absolute left-3.5 flex items-center justify-center size-5\">\n          <AnimatePresence mode=\"wait\">\n            {showFavicon ? (\n              <motion.img\n                key={`favicon-${domain}`}\n                src={getFaviconUrl(domain, faviconSize)}\n                alt={domain}\n                width={20}\n                height={20}\n                className=\"size-5 rounded-sm object-contain\"\n                initial={{ opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n                animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                exit={{ opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n                transition={{ type: \"spring\", stiffness: 400, damping: 28 }}\n                onLoad={() => setFaviconReady(true)}\n                onError={() => setFaviconError(true)}\n              />\n            ) : (\n              <motion.span\n                key=\"search-icon\"\n                initial={{ opacity: 0, scale: 0.7 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.7 }}\n                transition={{ type: \"spring\", stiffness: 400, damping: 28 }}\n                className=\"flex items-center justify-center text-muted-foreground\"\n              >\n                {domain && !faviconError ? (\n                  <Globe className=\"size-[18px]\" />\n                ) : (\n                  <Search className=\"size-[18px]\" />\n                )}\n              </motion.span>\n            )}\n          </AnimatePresence>\n\n          {/* preload img to detect load/error before showing the animated favicon */}\n          {domain && !faviconReady && !faviconError && (\n            <img\n              src={getFaviconUrl(domain, faviconSize)}\n              alt=\"\"\n              className=\"sr-only absolute\"\n              onLoad={() => setFaviconReady(true)}\n              onError={() => setFaviconError(true)}\n              aria-hidden\n            />\n          )}\n        </div>\n\n        <input\n          ref={ref}\n          type=\"text\"\n          value={value}\n          onChange={handleChange}\n          onKeyDown={handleKeyDown}\n          placeholder={placeholder}\n          className={cn(\n            \"flex w-full rounded-xl border border-border bg-background\",\n            \"pl-10 pr-10 py-2.5 text-sm text-foreground\",\n            \"placeholder:text-muted-foreground\",\n            \"outline-none ring-offset-background\",\n            \"transition-shadow duration-200\",\n            \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n            \"hover:border-muted-foreground/50\",\n            inputClassName,\n          )}\n        />\n\n        <AnimatePresence>\n          {clearable && value.length > 0 && (\n            <motion.button\n              type=\"button\"\n              onClick={handleClear}\n              initial={{ opacity: 0, scale: 0.7 }}\n              animate={{ opacity: 1, scale: 1 }}\n              exit={{ opacity: 0, scale: 0.7 }}\n              transition={{ type: \"spring\", stiffness: 400, damping: 28 }}\n              className={cn(\n                \"absolute right-3 flex items-center justify-center\",\n                \"size-5 rounded-full text-muted-foreground\",\n                \"hover:text-foreground hover:bg-muted transition-colors\",\n              )}\n              aria-label=\"Clear input\"\n            >\n              <X className=\"size-3.5\" />\n            </motion.button>\n          )}\n        </AnimatePresence>\n      </div>\n    );\n  },\n);\nFaviconSearch.displayName = \"FaviconSearch\";\n\nexport { FaviconSearch, extractDomain, getFaviconUrl };\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/favicon-search.tsx"
    }
  ],
  "type": "registry:ui"
}