diff --git a/CLAUDE.md b/CLAUDE.md
index afe615e..5546559 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,7 +35,8 @@ There are no test suites configured.
- **`store.ts`** — single Zustand store (`useGalleryStore`) that owns all app state and all `invoke()` calls to the Tauri backend. Every feature (folders, images, search, similar images, tags, captions, tagger, duplicates) is implemented as store actions here. React components are thin consumers.
- **`App.tsx`** — sets up Tauri event listeners (`subscribeToProgress`) and renders the top-level layout (sidebar + active view).
-- **`src/components/`** — UI components: `Gallery`, `Lightbox`, `Sidebar`, `Toolbar`, `TagCloud`, `DuplicateFinder`, `BackgroundTasks`, `SettingsModal`, `MenuBar`, `TitleBar`.
+- **`src/components/`** — UI components: `Gallery`, `Lightbox`, `Sidebar`, `Toolbar`, `Timeline`, `ExploreView`, `DuplicateFinder`, `BackgroundTasks`, `SettingsModal`, `TitleBar`.
+- **`src/components/menu/`** — shared floating-UI primitives: `useDismissable`, `MenuPanel`/`MenuItem`/`SubMenu`, the portal-based `ContextMenu`, and the app-wide `Dropdown`. Build menus, dropdowns, and popovers on these instead of hand-rolling.
- State management: Zustand v5, no selectors library — components call `useGalleryStore(s => s.field)` directly.
- Styling: Tailwind CSS v4 (Vite plugin, no config file).
- Virtualized gallery grid: `@tanstack/react-virtual`.
diff --git a/src/components/MenuBar.tsx b/src/components/MenuBar.tsx
deleted file mode 100644
index a48dbac..0000000
--- a/src/components/MenuBar.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { MediaFilter, ZoomPreset, useGalleryStore } from "../store";
-
-type MenuKey = "library" | "view" | "filter";
-
-function MenuButton({
- label,
- active,
- onClick,
-}: {
- label: string;
- active: boolean;
- onClick: () => void;
-}) {
- return (
-
- );
-}
-
-function MenuPanel({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-function MenuItem({
- label,
- hint,
- active = false,
- onClick,
-}: {
- label: string;
- hint?: string;
- active?: boolean;
- onClick: () => void;
-}) {
- return (
-
- );
-}
-
-const ZOOM_OPTIONS: { value: ZoomPreset; label: string }[] = [
- { value: "compact", label: "Compact Grid" },
- { value: "comfortable", label: "Comfortable Grid" },
- { value: "detail", label: "Detail Grid" },
-];
-
-const FILTER_OPTIONS: { value: MediaFilter; label: string }[] = [
- { value: "all", label: "All Media" },
- { value: "image", label: "Images" },
- { value: "video", label: "Videos" },
-];
-
-export function MenuBar() {
- const [openMenu, setOpenMenu] = useState(null);
- const rootRef = useRef(null);
- const setFolderPickerOpen = useGalleryStore((state) => state.setFolderPickerOpen);
- const reindexFolder = useGalleryStore((state) => state.reindexFolder);
- const selectedFolderId = useGalleryStore((state) => state.selectedFolderId);
- const zoomPreset = useGalleryStore((state) => state.zoomPreset);
- const setZoomPreset = useGalleryStore((state) => state.setZoomPreset);
- const mediaFilter = useGalleryStore((state) => state.mediaFilter);
- const setMediaFilter = useGalleryStore((state) => state.setMediaFilter);
- const favoritesOnly = useGalleryStore((state) => state.favoritesOnly);
- const setFavoritesOnly = useGalleryStore((state) => state.setFavoritesOnly);
-
- useEffect(() => {
- const handlePointerDown = (event: MouseEvent) => {
- if (!rootRef.current?.contains(event.target as Node)) {
- setOpenMenu(null);
- }
- };
-
- window.addEventListener("pointerdown", handlePointerDown);
- return () => window.removeEventListener("pointerdown", handlePointerDown);
- }, []);
-
- const handleAddFolder = () => {
- setFolderPickerOpen(true);
- setOpenMenu(null);
- };
-
- const handleReindex = async () => {
- if (selectedFolderId !== null) {
- await reindexFolder(selectedFolderId);
- }
- setOpenMenu(null);
- };
-
- return (
-
-
- setOpenMenu((current) => (current === "library" ? null : "library"))}
- />
- {openMenu === "library" ? (
-
-
-
-
- ) : null}
-
-
-
- setOpenMenu((current) => (current === "view" ? null : "view"))}
- />
- {openMenu === "view" ? (
-
- {ZOOM_OPTIONS.map((option) => (
-
- ) : null}
-
-
-
-
setOpenMenu((current) => (current === "filter" ? null : "filter"))}
- />
- {openMenu === "filter" ? (
-
- {FILTER_OPTIONS.map((option) => (
-
- ) : null}
-
-
- );
-}