}
+ renderAction={(c) => {
+ const inColl = existing.has(c.id) || added.has(c.id);
+ return (
+
+ );
+ }}
+ />
);
}
diff --git a/frontend/src/components/PlexLibraryPicker.tsx b/frontend/src/components/PlexLibraryPicker.tsx
new file mode 100644
index 00000000..25c0ba51
--- /dev/null
+++ b/frontend/src/components/PlexLibraryPicker.tsx
@@ -0,0 +1,60 @@
+import { type ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+import { useQuery } from "@tanstack/react-query";
+import { qk } from "../lib/queryKeys";
+import { api, type PlexCard } from "../lib/api";
+import { useDebounced } from "../lib/useDebounced";
+import { LoadingState } from "./QueryState";
+
+// A debounced library search-and-pick list, shared by the "Add title" panel (PlexCollectionView) and
+// the "New collection" first-title panel (plexBrowseLists) — both search the library the same way and
+// render the same result rows, differing only in the per-row action. Callers keep their own chrome
+// (search input, name field, close button) and supply `term` + `renderAction`; this owns the query,
+// the loading/error/empty states, and the row markup so those can't drift between the two surfaces.
+export default function PlexLibraryPicker({
+ scope,
+ term,
+ enabled = true,
+ emptyHint = null,
+ renderAction,
+}: {
+ scope: string;
+ /** The raw search term; debounced internally. */
+ term: string;
+ /** Gate the query off (e.g. until a collection name is valid). When false, renders nothing. */
+ enabled?: boolean;
+ /** Shown while the (debounced) term is empty — a hint, or nothing. */
+ emptyHint?: ReactNode;
+ /** The trailing control for each result row (Add / Create-with / …). */
+ renderAction: (card: PlexCard) => ReactNode;
+}) {
+ const { t } = useTranslation();
+ const debounced = useDebounced(term.trim(), 300);
+ const q = useQuery({
+ queryKey: qk.plexLibrary("collection-pick", scope, debounced),
+ queryFn: () => api.plexLibrary({ scope, q: debounced, limit: 12 }),
+ enabled: enabled && debounced.length > 0,
+ });
+ const results = q.data?.items ?? [];
+
+ if (!enabled) return null;
+ if (debounced.length === 0) return <>{emptyHint}>;
+ if (q.isLoading) return ;
+ if (q.isError) return