{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "oxide-header", "description": "A premium morphing dropdown navbar with animated panel transitions. Inspired by Oxide; supply your own menu content via the items prop.", "files": [ { "path": "src/registry/new-york/items/oxide-header/components/oxide-header.tsx", "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\"\n\nfunction Chevron({ active }: Readonly<{ active: boolean }>) {\n return (\n \n \n \n )\n}\n\nexport type OxideNavItem = {\n label: string\n content: ReactNode\n}\n\n// Palette exposed as CSS variables so menu content passed via `items` shares the\n// same tokens, and consumers can retheme by overriding any of them on the root.\nconst oxideTheme = {\n \"--oxide-nav-bg\": \"#0b0e12\",\n \"--oxide-panel-bg\": \"#111418\",\n \"--oxide-border\": \"#262b31\",\n \"--oxide-fg\": \"#d4d7db\",\n \"--oxide-fg-muted\": \"#b5b9bf\",\n \"--oxide-fg-subtle\": \"#8b9097\",\n \"--oxide-accent\": \"#00ffd1\",\n \"--oxide-accent-bg\": \"#003b34\",\n \"--oxide-accent-bg-hover\": \"#064c43\",\n \"--oxide-art-dim\": \"oklch(27.4% 0.006 286.033)\",\n \"--oxide-hover\": \"rgba(255, 255, 255, 0.06)\",\n \"--oxide-hover-subtle\": \"rgba(255, 255, 255, 0.025)\",\n} as CSSProperties\n\nconst easing = \"cubic-bezier(0.16, 1, 0.3, 1)\"\nconst morphTransition = `width 0.24s ${easing}, height 0.24s ${easing}, transform 0.24s ${easing}`\nconst fadeTransition = `opacity 0.16s ease, visibility 0.16s, transform 0.24s ${easing}`\n\nexport function OxideHeader({\n items,\n className,\n style,\n}: Readonly<{\n items: OxideNavItem[]\n className?: string\n style?: CSSProperties\n}>) {\n const [activeIndex, setActiveIndex] = useState(-1)\n const [isDropdownOpen, setIsDropdownOpen] = useState(false)\n const [wasOpen, setWasOpen] = useState(false)\n const [dimensions, setDimensions] = useState({ width: 0, height: 0, left: 0 })\n\n const navRef = useRef(null)\n const leaveTimeoutRef = useRef | null>(null)\n const sectionRefs = useRef>({})\n const navItemRefs = useRef>({})\n\n const clearLeaveTimeout = () => {\n if (leaveTimeoutRef.current) {\n clearTimeout(leaveTimeoutRef.current)\n leaveTimeoutRef.current = null\n }\n }\n\n const handleMouseEnter = (index: number) => {\n clearLeaveTimeout()\n setActiveIndex(index)\n\n const targetSection = sectionRefs.current[index]\n const navItem = navItemRefs.current[index]\n const navContainer = navRef.current\n\n if (!targetSection || !navItem || !navContainer) return\n\n const width = targetSection.offsetWidth\n const height = targetSection.offsetHeight\n const itemRect = navItem.getBoundingClientRect()\n const navRect = navContainer.getBoundingClientRect()\n\n const relativeLeft = itemRect.left - navRect.left\n const left = Math.max(16, relativeLeft + itemRect.width / 2 - width / 2)\n\n setDimensions({ width, height, left })\n setIsDropdownOpen(true)\n setWasOpen(true)\n }\n\n const handleMouseLeave = () => {\n leaveTimeoutRef.current = setTimeout(() => {\n setIsDropdownOpen(false)\n setActiveIndex(-1)\n }, 160)\n }\n\n useEffect(() => () => clearLeaveTimeout(), [])\n\n // Keep transitions off until the panel has opened once, so it never animates\n // out from a zero-size box on the very first hover.\n useEffect(() => {\n if (isDropdownOpen) return\n const timer = setTimeout(() => setWasOpen(false), 200)\n return () => clearTimeout(timer)\n }, [isDropdownOpen])\n\n const bgStyle: CSSProperties = {\n width: `${dimensions.width}px`,\n height: `${dimensions.height}px`,\n transform: `translateX(${dimensions.left}px) translateY(${isDropdownOpen ? 0 : 6}px)`,\n transition: wasOpen ? `${morphTransition}, ${fadeTransition}` : \"none\",\n }\n\n const getSectionStyle = (index: number): CSSProperties => {\n const isActive = activeIndex === index\n let translateX = 0\n if (activeIndex !== -1) {\n if (index < activeIndex) translateX = -10\n if (index > activeIndex) translateX = 10\n }\n\n return {\n position: \"absolute\",\n top: 0,\n left: 0,\n width: \"max-content\",\n pointerEvents: isActive ? \"auto\" : \"none\",\n opacity: isActive ? 1 : 0,\n transform: `translateX(${translateX}px)`,\n transition: isActive\n ? `opacity 0.22s ease 0.06s, transform 0.24s ${easing}`\n : \"opacity 0.14s ease, transform 0.16s ease\",\n }\n }\n\n const visibilityClass = isDropdownOpen\n ? \"pointer-events-auto visible opacity-100\"\n : \"invisible opacity-0\"\n\n const navButtonClass =\n \"flex h-[46px] cursor-pointer items-center border-none bg-transparent px-4 font-mono text-[12px] font-semibold tracking-[0.12em] text-[var(--oxide-fg-subtle)] uppercase outline-none transition-colors duration-150 hover:text-[var(--oxide-fg)]\"\n\n return (\n \n \n \n )\n}\n", "type": "registry:ui" } ], "type": "registry:component" }