diff --git a/src/pages/library/controls.tsx b/src/pages/library/controls.tsx
new file mode 100644
index 0000000..86f1dec
--- /dev/null
+++ b/src/pages/library/controls.tsx
@@ -0,0 +1,112 @@
+import { useMemo, useState } from 'react'
+import { Bookmark, Trash2 } from '../../lib/icons'
+import { useSavedSearches, type SavedSearchFilters } from '../../stores/saved-searches-store'
+
+export function WatchedPills({
+ value,
+ onChange,
+}: {
+ value: 'any' | 'played' | 'unplayed'
+ onChange: (v: 'any' | 'played' | 'unplayed') => void
+}) {
+ const opts: Array<{ key: 'any' | 'played' | 'unplayed'; label: string }> = [
+ { key: 'any', label: 'All' },
+ { key: 'unplayed', label: 'Unwatched' },
+ { key: 'played', label: 'Watched' },
+ ]
+ return (
+
+ {opts.map(o => {
+ const on = value === o.key
+ return (
+
+ )
+ })}
+
+ )
+}
+
+export function SavedSearchMenu({
+ scope,
+ onApply,
+ onSave,
+}: {
+ scope: 'movies' | 'shows'
+ onApply: (filters: SavedSearchFilters) => void
+ onSave: () => void
+}) {
+ // Select the whole array; filter in render. Returning a fresh array from
+ // the selector breaks Zustand's snapshot identity check and sends
+ // useSyncExternalStore into an update loop.
+ const allSearches = useSavedSearches(s => s.searches)
+ const searches = useMemo(
+ () => allSearches.filter(x => x.scope === scope),
+ [allSearches, scope],
+ )
+ const remove = useSavedSearches(s => s.remove)
+ const [open, setOpen] = useState(false)
+ return (
+
+
+ {open && (
+ <>
+
setOpen(false)} />
+
+
+ {searches.length === 0 ? (
+
+ No saved searches yet.
+
+ ) : (
+ searches.map(s => (
+
+
+
+
+ ))
+ )}
+
+ >
+ )}
+
+ )
+}
diff --git a/src/pages/library/modals.tsx b/src/pages/library/modals.tsx
new file mode 100644
index 0000000..df4e992
--- /dev/null
+++ b/src/pages/library/modals.tsx
@@ -0,0 +1,167 @@
+import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { motion, AnimatePresence } from 'framer-motion'
+import { Shuffle } from '../../lib/icons'
+import { useSavedSearches, type SavedSearchFilters } from '../../stores/saved-searches-store'
+import type { BaseItemDto } from '../../api/types'
+
+export function SaveSearchModal({
+ open,
+ scope,
+ filters,
+ onClose,
+}: {
+ open: boolean
+ scope: 'movies' | 'shows'
+ filters: SavedSearchFilters
+ onClose: () => void
+}) {
+ const add = useSavedSearches(s => s.add)
+ const [name, setName] = useState('')
+ function save() {
+ if (!name.trim()) return
+ add(scope, name, filters)
+ setName('')
+ onClose()
+ }
+ return (
+
+ {open && (
+
+ e.stopPropagation()}
+ className="relative w-full max-w-sm rounded-2xl bg-surface ring-1 ring-border-strong shadow-[0_30px_80px_-20px_rgba(0,0,0,0.85)] p-5"
+ >
+
+ Save this search
+
+
+ Capture the current filters under a name you can recall later.
+
+ setName(e.target.value)}
+ onKeyDown={e => { if (e.key === 'Enter') save() }}
+ placeholder='e.g. "Unwatched 4K HDR"'
+ autoFocus
+ className="w-full h-10 px-3 rounded-md bg-elevated/60 ring-1 ring-border focus:ring-accent/50 outline-none text-[13px] tracking-tight"
+ />
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export function SurpriseMeModal({
+ open,
+ items,
+ onClose,
+}: {
+ open: boolean
+ items: BaseItemDto[]
+ onClose: () => void
+}) {
+ const navigate = useNavigate()
+ const [pick, setPick] = useState
(null)
+
+ function roll() {
+ if (items.length === 0) {
+ setPick(null)
+ return
+ }
+ const idx = Math.floor(Math.random() * items.length)
+ setPick(items[idx])
+ }
+
+ // Roll a fresh pick when the modal opens (or items change while open),
+ // and clear when it closes.
+ useEffect(() => {
+ if (open && items.length > 0) {
+ const idx = Math.floor(Math.random() * items.length)
+ setPick(items[idx])
+ } else if (!open) {
+ setPick(null)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open, items.length])
+
+ return (
+
+ {open && pick && (
+
+ e.stopPropagation()}
+ className="relative w-full max-w-md rounded-2xl bg-surface ring-1 ring-border-strong shadow-[0_30px_80px_-20px_rgba(0,0,0,0.85)] p-5"
+ >
+
+ Surprise me
+
+
+ {pick.Name}
+
+
+ {pick.Overview || 'No description available.'}
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/src/pages/library/states.tsx b/src/pages/library/states.tsx
new file mode 100644
index 0000000..c4216a9
--- /dev/null
+++ b/src/pages/library/states.tsx
@@ -0,0 +1,36 @@
+import { Film } from '../../lib/icons'
+import { usePosterGridClasses } from '../../lib/density'
+
+export function EmptyLibrary({ type, Icon }: { type: string; Icon: typeof Film }) {
+ return (
+
+
+
No {type} found
+
+ Add media to your Jellyfin library to see it here.
+
+
+ )
+}
+
+export function PosterGridSkeleton() {
+ const gridCls = usePosterGridClasses()
+ return (
+
+ {Array.from({ length: 24 }).map((_, i) => (
+
+ ))}
+
+ )
+}