"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Search, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { loadIconsManifest } from "@/lib/icons-manifest"; import type { IconEntry } from "@/lib/icons"; interface QuickImportProps { /** Receives raw SVG XML string and the icon's title (for export filename / pen title). */ onImport: (svg: string, title: string) => void; className?: string; } const STARTER_SLUGS = [ "gmail-2026", "google-drive-2026", "google-calendar-2026", "google-meet-2026", "github", "figma", "vercel", "stripe", ]; export function QuickImport({ onImport, className }: QuickImportProps) { const [icons, setIcons] = useState(null); const [query, setQuery] = useState(""); const [open, setOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(0); const [loadingSlug, setLoadingSlug] = useState(null); const containerRef = useRef(null); useEffect(() => { let cancelled = false; loadIconsManifest() .then((data) => { if (!cancelled) setIcons(data); }) .catch(() => { if (!cancelled) setIcons([]); }); return () => { cancelled = true; }; }, []); useEffect(() => { function onPointerDown(e: PointerEvent) { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setOpen(false); } } document.addEventListener("pointerdown", onPointerDown); return () => document.removeEventListener("pointerdown", onPointerDown); }, []); const starterIcons = useMemo(() => { if (!icons) return []; const lookup = new Map(icons.map((i) => [i.slug, i])); return STARTER_SLUGS.map((s) => lookup.get(s)).filter( (x): x is IconEntry => Boolean(x), ); }, [icons]); const matches = useMemo(() => { if (!icons || !query.trim()) return []; const q = query.trim().toLowerCase(); const result: IconEntry[] = []; for (const icon of icons) { if (result.length >= 8) break; if ( icon.slug.toLowerCase().includes(q) || icon.title.toLowerCase().includes(q) || icon.aliases.some((a) => a.toLowerCase().includes(q)) ) { result.push(icon); } } return result; }, [icons, query]); const loadIcon = useCallback( async (icon: IconEntry) => { setLoadingSlug(icon.slug); try { const res = await fetch(icon.variants.default); if (!res.ok) { // Surface the failure so the viewer's status bar can flip to // "invalid" with a useful error instead of receiving the // origin's HTML error page as garbage SVG content. throw new Error(`Failed to load ${icon.slug} (${res.status})`); } const text = await res.text(); onImport(text, icon.title); setOpen(false); setQuery(""); } catch { // swallow; the viewer's error state will surface failures on its side. } finally { setLoadingSlug(null); } }, [onImport], ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!matches.length) return; if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, matches.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); const pick = matches[activeIndex]; if (pick) void loadIcon(pick); } else if (e.key === "Escape") { setOpen(false); } }, [matches, activeIndex, loadIcon], ); return (
{/* Search */}
{ setQuery(e.target.value); setOpen(true); setActiveIndex(0); }} onFocus={() => setOpen(true)} onKeyDown={handleKeyDown} placeholder={ icons === null ? "Loading icon library..." : "Import from library — try 'gmail 2026', 'github'..." } disabled={icons === null} className="flex-1 bg-transparent text-xs text-foreground outline-none placeholder:text-muted-foreground/50 disabled:cursor-wait" />
{open && matches.length > 0 && (
    {matches.map((icon, i) => (
  • ))}
)}
{/* Starter chips */} {starterIcons.length > 0 && (
Try {starterIcons.map((icon) => ( ))}
)}
); }