"use client"; import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react"; import { cn } from "@/lib/utils"; export type ThemeValue = "light" | "dark" | "system"; export type ProfileMenuStatusItem = { text: string; icon?: ReactNode; }; export type StatusInput = string | ProfileMenuStatusItem; export type ProfileMenuItem = { name: string; icon?: ReactNode; shortcut?: string; badge?: ReactNode; active?: boolean; centered?: boolean; danger?: boolean; closeOnSelect?: boolean; onSelect?: () => void; }; export type ProfileMenuSection = { label?: string; grid?: boolean; items: ProfileMenuItem[]; }; export interface ProfileMenuProps { title?: ReactNode; subtitle?: ReactNode; avatarSrc?: string; avatar?: ReactNode; status?: StatusInput[]; statusInterval?: number; sections?: ProfileMenuSection[]; theme?: ThemeValue; onThemeChange?: (theme: ThemeValue) => void; shortcutKey?: string; online?: boolean; id?: string; className?: string; } const DEFAULT_AVATAR = "https://i.pinimg.com/736x/c9/3d/c8/c93dc8fdae8629e56c85e7b2a10ffd7b.jpg"; const DEFAULT_STATUS_ITEMS: ProfileMenuStatusItem[] = [ { text: "Designing interactive systems", icon: , }, { text: "Available for select projects", icon: , }, { text: "San Francisco, CA · 10:42 AM", icon: , }, { text: "Design Engineer @ DevClub", icon: , }, ]; const STYLES = ` @keyframes pm-slide-in { 0% { opacity: 0; transform: translateY(5px); filter: blur(2px); } 100% { opacity: 1; transform: translateY(0); filter: blur(0); } } .pm-status { animation: pm-slide-in 0.3s ease-out backwards; } @media (prefers-reduced-motion: reduce) { .pm-status { animation: none; } } `; const subscribePlatform = () => () => { }; const getPlatformSnapshot = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform); const getServerSnapshot = () => false; export function ProfileMenu({ title = "Yatharth K.", avatarSrc = DEFAULT_AVATAR, avatar, status, statusInterval = 3600, sections, theme, onThemeChange, shortcutKey = "k", online = true, id = "profile-command-menu", className, }: ProfileMenuProps) { const isApple = useSyncExternalStore( subscribePlatform, getPlatformSnapshot, getServerSnapshot, ); const [isOpen, setIsOpen] = useState(false); const [isPinned, setIsPinned] = useState(false); const [selected, setSelected] = useState(0); const [statusIndex, setStatusIndex] = useState(0); const [copied, setCopied] = useState(false); const [internalTheme, setInternalTheme] = useState(() => { if (typeof window === "undefined") return "dark"; const stored = localStorage.getItem("theme"); if (stored === "light" || stored === "dark" || stored === "system") { return stored; } return document.documentElement.classList.contains("dark") ? "dark" : "light"; }); const activeTheme = theme ?? internalTheme; const panelRef = useRef(null); const highlightRef = useRef(null); const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); const hoverTimeoutRef = useRef | null>(null); const rootRef = useRef(null); const triggerRef = useRef(null); const pendingFocusRef = useRef(null); const rawStatuses = status ?? DEFAULT_STATUS_ITEMS; const statusItems: ProfileMenuStatusItem[] = rawStatuses.map((s) => typeof s === "string" ? { text: s } : s, ); const handleCopyEmail = useCallback(() => { if (typeof navigator !== "undefined" && navigator.clipboard) { navigator.clipboard.writeText("hello@devclub.co"); } setCopied(true); setTimeout(() => { setCopied(false); }, 2000); }, []); useEffect(() => { if (typeof window === "undefined") return; const syncTheme = () => { const stored = localStorage.getItem("theme"); if (stored === "light" || stored === "dark" || stored === "system") { setInternalTheme(stored); } else { setInternalTheme( document.documentElement.classList.contains("dark") ? "dark" : "light", ); } }; const observer = new MutationObserver(() => { if (!localStorage.getItem("theme")) { syncTheme(); } }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"], }); window.addEventListener("storage", syncTheme); window.addEventListener("theme-change", syncTheme); return () => { observer.disconnect(); window.removeEventListener("storage", syncTheme); window.removeEventListener("theme-change", syncTheme); }; }, []); const handleThemeSelect = useCallback( (nextTheme: ThemeValue) => { setInternalTheme(nextTheme); onThemeChange?.(nextTheme); if (typeof document !== "undefined") { if (nextTheme === "dark") { document.documentElement.classList.add("dark"); document.documentElement.style.colorScheme = "dark"; try { localStorage.setItem("theme", "dark"); } catch { } } else if (nextTheme === "light") { document.documentElement.classList.remove("dark"); document.documentElement.style.colorScheme = "light"; try { localStorage.setItem("theme", "light"); } catch { } } else { const isDark = window.matchMedia( "(prefers-color-scheme: dark)", ).matches; document.documentElement.classList.toggle("dark", isDark); document.documentElement.style.colorScheme = isDark ? "dark" : "light"; try { localStorage.setItem("theme", "system"); } catch { } } window.dispatchEvent( new StorageEvent("storage", { key: "theme", newValue: nextTheme, }), ); window.dispatchEvent( new CustomEvent("theme-change", { detail: { theme: nextTheme }, }), ); } }, [onThemeChange], ); const defaultSections: ProfileMenuSection[] = [ { label: "Navigation", items: [ { name: "View Profile", icon: , shortcut: "⌘P", closeOnSelect: true, onSelect: () => { window.open("https://github.com/devclub-nstru", "_blank"); }, }, { name: "Projects & Works", icon: , shortcut: "⌘J", closeOnSelect: true, onSelect: () => { window.open("https://github.com/devclub-nstru", "_blank"); }, }, { name: copied ? "Email Copied!" : "Copy Email", icon: copied ? : , shortcut: copied ? undefined : "⌘C", badge: copied ? ( Copied! ) : undefined, closeOnSelect: false, onSelect: handleCopyEmail, }, { name: "Book a Call", icon: , shortcut: "⌘B", closeOnSelect: true, onSelect: () => { window.open("https://cal.com", "_blank"); }, }, ], }, { label: "Social", items: [ { name: "GitHub", icon: , badge: , closeOnSelect: true, onSelect: () => { window.open("https://github.com/devclub-nstru", "_blank"); }, }, { name: "Twitter / X", icon: , badge: , closeOnSelect: true, onSelect: () => { window.open("https://x.com", "_blank"); }, }, ], }, ]; const resolvedSections: ProfileMenuSection[] = [ ...(sections || defaultSections), ]; resolvedSections.push({ label: "Appearance", grid: true, items: [ { name: "Light", icon: , centered: true, active: activeTheme === "light", closeOnSelect: false, onSelect: () => handleThemeSelect("light"), }, { name: "Dark", icon: , centered: true, active: activeTheme === "dark", closeOnSelect: false, onSelect: () => handleThemeSelect("dark"), }, { name: "System", icon: , centered: true, active: activeTheme === "system", closeOnSelect: false, onSelect: () => handleThemeSelect("system"), }, ], }); const items = resolvedSections.flatMap((s) => s.items); const itemCount = items.length; const triggerId = `${id}-trigger`; const menuId = `${id}-menu`; useLayoutEffect(() => { const hl = highlightRef.current; if (!hl) return; if (!isOpen || selected < 0) { hl.style.opacity = "0"; return; } const element = itemRefs.current[selected]; if (!element) return; hl.style.transform = `translate3d(${element.offsetLeft}px, ${element.offsetTop}px, 0)`; hl.style.width = `${element.offsetWidth}px`; hl.style.height = `${element.offsetHeight}px`; hl.style.opacity = "1"; }, [isOpen, selected]); useLayoutEffect(() => { if (!isOpen || pendingFocusRef.current === null) return; itemRefs.current[pendingFocusRef.current]?.focus({ preventScroll: true }); pendingFocusRef.current = null; }, [isOpen]); useEffect(() => { if (isOpen || statusItems.length < 2) return; const interval = setInterval(() => { if (!document.hidden) { setStatusIndex((prev) => (prev + 1) % statusItems.length); } }, statusInterval); return () => clearInterval(interval); }, [isOpen, statusItems.length, statusInterval]); const clearHoverTimeout = useCallback(() => { if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; } }, []); const focusItem = useCallback((index: number) => { setSelected(index); itemRefs.current[index]?.focus({ preventScroll: true }); }, []); const openMenu = useCallback( (focusIndex: number | null) => { clearHoverTimeout(); setSelected(focusIndex ?? 0); setIsPinned(true); if (isOpen) { if (focusIndex !== null) focusItem(focusIndex); return; } pendingFocusRef.current = focusIndex; setIsOpen(true); }, [clearHoverTimeout, focusItem, isOpen], ); const closeMenu = useCallback(() => { clearHoverTimeout(); setIsOpen(false); setIsPinned(false); if (rootRef.current?.contains(document.activeElement)) { triggerRef.current?.focus({ preventScroll: true }); } }, [clearHoverTimeout]); const toggleMenu = useCallback(() => { if (isOpen) closeMenu(); else openMenu(0); }, [closeMenu, isOpen, openMenu]); const handleTriggerClick = useCallback(() => { if (isOpen && isPinned) closeMenu(); else openMenu(null); }, [closeMenu, isOpen, isPinned, openMenu]); const handleTriggerKeyDown = (event: React.KeyboardEvent) => { switch (event.key) { case "Enter": case " ": event.preventDefault(); if (isOpen && isPinned) closeMenu(); else openMenu(0); break; case "ArrowDown": event.preventDefault(); openMenu(0); break; case "ArrowUp": event.preventDefault(); openMenu(itemCount - 1); break; } }; const handleMenuKeyDown = (event: React.KeyboardEvent) => { if (itemCount === 0) return; const current = itemRefs.current.findIndex( (node) => node === document.activeElement, ); const from = current < 0 ? selected : current; switch (event.key) { case "ArrowDown": case "j": event.preventDefault(); focusItem((from + 1) % itemCount); break; case "ArrowUp": case "k": event.preventDefault(); focusItem((from - 1 + itemCount) % itemCount); break; case "Home": event.preventDefault(); focusItem(0); break; case "End": event.preventDefault(); focusItem(itemCount - 1); break; case "Tab": clearHoverTimeout(); setIsOpen(false); setIsPinned(false); break; } }; const handlePointerEnter = useCallback( (event: React.PointerEvent) => { if (event.pointerType !== "mouse") return; clearHoverTimeout(); if (!isOpen) { setSelected(0); setIsOpen(true); } }, [clearHoverTimeout, isOpen], ); const handlePointerLeave = useCallback( (event: React.PointerEvent) => { if (event.pointerType !== "mouse" || isPinned) return; hoverTimeoutRef.current = setTimeout(closeMenu, 200); }, [closeMenu, isPinned], ); useEffect(() => { function onKeyDown(event: KeyboardEvent) { const isCmdOrCtrl = isApple ? event.metaKey : event.ctrlKey; if ( event.key.toLowerCase() === shortcutKey.toLowerCase() && isCmdOrCtrl ) { event.preventDefault(); toggleMenu(); } } window.addEventListener("keydown", onKeyDown); return () => { window.removeEventListener("keydown", onKeyDown); }; }, [isApple, shortcutKey, toggleMenu]); useEffect(() => { if (!isOpen) return; function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { event.preventDefault(); closeMenu(); } } function onClickOutside(event: MouseEvent) { if (!(event.target as HTMLElement).closest(`#${id}`)) { closeMenu(); } } window.addEventListener("keydown", onKeyDown); window.addEventListener("click", onClickOutside); return () => { window.removeEventListener("keydown", onKeyDown); window.removeEventListener("click", onClickOutside); }; }, [closeMenu, isOpen, id]); const currentStatus = statusItems[statusIndex]; return ( {avatar ? ( avatar ) : ( )} {online && ( )} {title} {currentStatus && ( {currentStatus.icon && ( {currentStatus.icon} )} {currentStatus.text} )} {isApple ? "⌘" : "Ctrl"} {shortcutKey} {resolvedSections.map((section, sIdx) => ( {sIdx > 0 && ( )} {section.label && ( {section.label} )} {section.items.map((item) => { const index = items.indexOf(item); return ( { itemRefs.current[index] = node; }} type="button" role={ item.active === undefined ? "menuitem" : "menuitemradio" } aria-checked={item.active} tabIndex={-1} onMouseEnter={() => setSelected(index)} onFocus={() => setSelected(index)} onClick={(e) => { e.stopPropagation(); item.onSelect?.(); if (item.closeOnSelect) closeMenu(); }} className={cn( "relative z-10 flex w-full cursor-pointer items-center gap-2.5 rounded-xl px-2.5 py-2 text-xs font-sans transition-colors", item.centered ? "justify-center" : "justify-between", item.active ? "text-foreground font-medium ring-1 ring-border/80 dark:ring-white/20 bg-foreground/5 dark:bg-white/5" : "text-muted-foreground hover:text-foreground", item.danger && "text-rose-500 dark:text-rose-400 hover:text-rose-600", )} > {item.icon && ( {item.icon} )} {item.name} {item.shortcut && !item.centered && ( {item.shortcut} )} {item.badge && !item.centered && ( {item.badge} )} ); })} ))} ); } function UserIcon() { return ( ); } function FolderIcon() { return ( ); } function CopyIcon() { return ( ); } function CheckIcon() { return ( ); } function CalendarIcon() { return ( ); } function GithubIcon() { return ( ); } function TwitterIcon() { return ( ); } function SunIcon() { return ( ); } function MoonIcon() { return ( ); } function SystemIcon() { return ( ); } function ChevronDownIcon({ className }: { className?: string }) { return ( ); } function SparklesIcon() { return ( ); } function ZapIcon() { return ( ); } function MapPinIcon() { return ( ); } function TerminalIcon() { return ( ); } function ExternalLinkIcon() { return ( ); }