import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router';
import { Search } from 'lucide-react';
import { rank } from './match';
import { useSpotlightItems, type SpotlightItem } from './useSpotlightItems';
// Spotlight: type a few letters, go somewhere.
//
// The dock is full and cannot grow, which is what this is for — once navigation lives here, the dock is
// free to be a short list of what you actually use rather than a complete one.
//
// ── Keyboard first, and the trigger ──
//
// `ctrl-space`. `alt-space` is the more macOS-shaped chord and is already load-bearing for the owner
// elsewhere, so it is deliberately not taken. `cmd-k` is not used either: it belongs to the browser's own
// surface on some platforms and to text fields on others.
//
// Escape closes, arrows move, enter goes. The mouse works, but nothing here is designed around it.
const MAX_RESULTS = 8;
/** Label with the matched characters picked out, so it is obvious WHY a result is here. */
const Highlighted = ({ label, hits }: { label: string; hits: number[] }) => {
if (!hits.length) return <>{label}>;
const set = new Set(hits);
return (
<>
{label.split('').map((char, i) =>
set.has(i) ? (
{char}
) : (
{char}
),
)}
>
);
};
const Row = ({
item,
hits,
active,
onPick,
}: {
item: SpotlightItem;
hits: number[];
active: boolean;
onPick: () => void;
}) => {
const Icon = item.icon;
return (
);
};
export const Spotlight = () => {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const [active, setActive] = useState(0);
const inputRef = useRef(null);
const navigate = useNavigate();
const items = useSpotlightItems();
const results = useMemo(() => rank(items, query).slice(0, MAX_RESULTS), [items, query]);
const close = useCallback(() => {
setOpen(false);
setQuery('');
setActive(0);
}, []);
const pick = useCallback(
(item: SpotlightItem | undefined) => {
if (!item) return;
close();
navigate(item.to);
},
[close, navigate],
);
// The trigger. Bound to the window rather than to a container, because it has to work while focus is
// anywhere — including inside a panel that has its own key handling.
useEffect(() => {
const onKey = (ev: KeyboardEvent) => {
if (ev.ctrlKey && ev.code === 'Space') {
ev.preventDefault();
setOpen((wasOpen) => !wasOpen);
return;
}
if (ev.key === 'Escape' && open) close();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, close]);
useEffect(() => {
if (open) inputRef.current?.focus();
}, [open]);
// Reset the cursor whenever the result set changes, so it never points past the end.
useEffect(() => setActive(0), [query]);
if (!open) return null;
return (