{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "notifications-filter", "type": "registry:ui", "title": "Notifications Filter", "description": "Spring pill bar with layoutId sliding indicator and staggered item transitions.", "dependencies": [ "motion" ], "registryDependencies": [], "files": [ { "path": "registry/ruixenui/notifications-filter.tsx", "content": "\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\n/**\n * Spring Pill Bar — Rauno Freiberg craft.\n *\n * Filter categories are pill buttons with a sliding indicator\n * that spring-animates between active positions via layoutId.\n * Notifications spring in/out on filter change with stagger.\n */\n\n/* ── Audio ── */\n\nlet _a: AudioContext | null = null;\n\nfunction tick() {\n try {\n if (!_a) _a = new AudioContext();\n const buf = _a.createBuffer(\n 1,\n Math.floor(_a.sampleRate * 0.003),\n _a.sampleRate,\n );\n const d = buf.getChannelData(0);\n for (let i = 0; i < d.length; i++)\n d[i] = (Math.random() * 2 - 1) * (1 - i / d.length) ** 4;\n const src = _a.createBufferSource();\n src.buffer = buf;\n const g = _a.createGain();\n g.gain.value = 0.06;\n src.connect(g).connect(_a.destination);\n src.start();\n } catch {\n /* silent */\n }\n}\n\n/* ── Types ── */\n\ninterface FilterItem {\n id: string;\n title: string;\n body: string;\n time: string;\n category: string;\n}\n\ninterface NotificationsFilterProps {\n items?: FilterItem[];\n categories?: string[];\n sound?: boolean;\n}\n\n/* ── Defaults ── */\n\nconst DEFAULT_CATEGORIES = [\"All\", \"Updates\", \"Alerts\", \"Messages\"];\n\nconst DEFAULT_ITEMS: FilterItem[] = [\n {\n id: \"1\",\n title: \"New deployment\",\n body: \"v2.4.1 deployed to production\",\n time: \"2m\",\n category: \"Updates\",\n },\n {\n id: \"2\",\n title: \"Security alert\",\n body: \"New login from unknown device\",\n time: \"8m\",\n category: \"Alerts\",\n },\n {\n id: \"3\",\n title: \"Message from Alex\",\n body: \"Hey, can you review my PR?\",\n time: \"15m\",\n category: \"Messages\",\n },\n {\n id: \"4\",\n title: \"Build passed\",\n body: \"Pipeline #846 completed successfully\",\n time: \"24m\",\n category: \"Updates\",\n },\n {\n id: \"5\",\n title: \"Rate limit warning\",\n body: \"API approaching rate limit threshold\",\n time: \"1h\",\n category: \"Alerts\",\n },\n {\n id: \"6\",\n title: \"Message from Sarah\",\n body: \"The design review is scheduled for 3pm\",\n time: \"2h\",\n category: \"Messages\",\n },\n {\n id: \"7\",\n title: \"Package update\",\n body: \"3 dependencies have available updates\",\n time: \"4h\",\n category: \"Updates\",\n },\n {\n id: \"8\",\n title: \"Downtime alert\",\n body: \"Scheduled maintenance at midnight\",\n time: \"6h\",\n category: \"Alerts\",\n },\n];\n\n/* ── CSS ── */\n\nconst CSS = `.nf{--nf-glass:linear-gradient(135deg,rgba(255,255,255,.78),rgba(255,255,255,.62));--nf-border:rgba(0,0,0,.06);--nf-shadow:0 8px 32px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.04);--nf-hi:rgba(0,0,0,.88);--nf-dim:rgba(0,0,0,.35);--nf-sep:rgba(0,0,0,.06);--nf-pill-bg:rgba(0,0,0,.88);--nf-pill-fg:rgba(255,255,255,.95);--nf-pill-idle:rgba(0,0,0,.45);--nf-hover:rgba(0,0,0,.03)}.dark .nf,[data-theme=\"dark\"] .nf{--nf-glass:linear-gradient(135deg,rgba(255,255,255,.05),rgba(255,255,255,.02));--nf-border:rgba(255,255,255,.07);--nf-shadow:0 8px 32px rgba(0,0,0,.32),0 1px 2px rgba(0,0,0,.16);--nf-hi:rgba(255,255,255,.88);--nf-dim:rgba(255,255,255,.35);--nf-sep:rgba(255,255,255,.06);--nf-pill-bg:rgba(255,255,255,.88);--nf-pill-fg:rgba(0,0,0,.9);--nf-pill-idle:rgba(255,255,255,.45);--nf-hover:rgba(255,255,255,.03)}`;\n\n/* ── Component ── */\n\nexport function NotificationsFilter({\n items = DEFAULT_ITEMS,\n categories = DEFAULT_CATEGORIES,\n sound = true,\n}: NotificationsFilterProps) {\n const [active, setActive] = useState(categories[0]);\n const [hoveredId, setHoveredId] = useState(null);\n\n const filtered =\n active === \"All\" ? items : items.filter((i) => i.category === active);\n\n const handleCategory = useCallback(\n (cat: string) => {\n if (cat === active) return;\n if (sound) tick();\n setActive(cat);\n },\n [active, sound],\n );\n\n return (\n \n