{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "CardNav-JS-CSS", "type": "registry:block", "title": "CardNav", "description": "Expandable navigation bar with card panels revealing nested links.", "dependencies": [ "gsap" ], "files": [ { "path": "public/default/src/content/Components/CardNav/CardNav.jsx", "content": "import { useLayoutEffect, useRef, useState } from 'react';\r\nimport { gsap } from 'gsap';\r\n// use your own icon import if react-icons is not available\r\nimport { GoArrowUpRight } from 'react-icons/go';\r\nimport './CardNav.css';\r\n\r\nconst CardNav = ({\r\n logo,\r\n logoAlt = 'Logo',\r\n items,\r\n className = '',\r\n ease = 'power3.out',\r\n baseColor = '#fff',\r\n menuColor,\r\n buttonBgColor,\r\n buttonTextColor\r\n}) => {\r\n const [isHamburgerOpen, setIsHamburgerOpen] = useState(false);\r\n const [isExpanded, setIsExpanded] = useState(false);\r\n const navRef = useRef(null);\r\n const cardsRef = useRef([]);\r\n const tlRef = useRef(null);\r\n\r\n const calculateHeight = () => {\r\n const navEl = navRef.current;\r\n if (!navEl) return 260;\r\n\r\n const isMobile = window.matchMedia('(max-width: 768px)').matches;\r\n if (isMobile) {\r\n const contentEl = navEl.querySelector('.card-nav-content');\r\n if (contentEl) {\r\n const wasVisible = contentEl.style.visibility;\r\n const wasPointerEvents = contentEl.style.pointerEvents;\r\n const wasPosition = contentEl.style.position;\r\n const wasHeight = contentEl.style.height;\r\n\r\n contentEl.style.visibility = 'visible';\r\n contentEl.style.pointerEvents = 'auto';\r\n contentEl.style.position = 'static';\r\n contentEl.style.height = 'auto';\r\n\r\n contentEl.offsetHeight;\r\n\r\n const topBar = 60;\r\n const padding = 16;\r\n const contentHeight = contentEl.scrollHeight;\r\n\r\n contentEl.style.visibility = wasVisible;\r\n contentEl.style.pointerEvents = wasPointerEvents;\r\n contentEl.style.position = wasPosition;\r\n contentEl.style.height = wasHeight;\r\n\r\n return topBar + contentHeight + padding;\r\n }\r\n }\r\n return 260;\r\n };\r\n\r\n const createTimeline = () => {\r\n const navEl = navRef.current;\r\n if (!navEl) return null;\r\n\r\n gsap.set(navEl, { height: 60, overflow: 'hidden' });\r\n gsap.set(cardsRef.current, { y: 50, opacity: 0 });\r\n\r\n const tl = gsap.timeline({ paused: true });\r\n\r\n tl.to(navEl, {\r\n height: calculateHeight,\r\n duration: 0.4,\r\n ease\r\n });\r\n\r\n tl.to(cardsRef.current, { y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 }, '-=0.1');\r\n\r\n return tl;\r\n };\r\n\r\n useLayoutEffect(() => {\r\n const tl = createTimeline();\r\n tlRef.current = tl;\r\n\r\n return () => {\r\n tl?.kill();\r\n tlRef.current = null;\r\n };\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [ease, items]);\r\n\r\n useLayoutEffect(() => {\r\n const handleResize = () => {\r\n if (!tlRef.current) return;\r\n\r\n if (isExpanded) {\r\n const newHeight = calculateHeight();\r\n gsap.set(navRef.current, { height: newHeight });\r\n\r\n tlRef.current.kill();\r\n const newTl = createTimeline();\r\n if (newTl) {\r\n newTl.progress(1);\r\n tlRef.current = newTl;\r\n }\r\n } else {\r\n tlRef.current.kill();\r\n const newTl = createTimeline();\r\n if (newTl) {\r\n tlRef.current = newTl;\r\n }\r\n }\r\n };\r\n\r\n window.addEventListener('resize', handleResize);\r\n return () => window.removeEventListener('resize', handleResize);\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isExpanded]);\r\n\r\n const toggleMenu = () => {\r\n const tl = tlRef.current;\r\n if (!tl) return;\r\n if (!isExpanded) {\r\n setIsHamburgerOpen(true);\r\n setIsExpanded(true);\r\n tl.play(0);\r\n } else {\r\n setIsHamburgerOpen(false);\r\n tl.eventCallback('onReverseComplete', () => setIsExpanded(false));\r\n tl.reverse();\r\n }\r\n };\r\n\r\n const setCardRef = i => el => {\r\n if (el) cardsRef.current[i] = el;\r\n };\r\n\r\n return (\r\n
\r\n \r\n
\r\n );\r\n};\r\n\r\nexport default CardNav;\r\n", "type": "registry:component" }, { "path": "public/default/src/content/Components/CardNav/CardNav.css", "content": ".card-nav-container {\r\n position: absolute;\r\n top: 2em;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n width: 90%;\r\n max-width: 800px;\r\n z-index: 99;\r\n box-sizing: border-box;\r\n}\r\n\r\n.card-nav {\r\n display: block;\r\n height: 60px;\r\n padding: 0;\r\n background-color: white;\r\n border: 0.5px solid rgba(255, 255, 255, 0.1);\r\n border-radius: 0.75rem;\r\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\r\n position: relative;\r\n overflow: hidden;\r\n will-change: height;\r\n}\r\n\r\n.card-nav-top {\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n height: 60px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 0.5rem 0.45rem 0.55rem 1.1rem;\r\n z-index: 2;\r\n}\r\n\r\n.hamburger-menu {\r\n height: 100%;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n gap: 6px;\r\n}\r\n\r\n.hamburger-menu:hover .hamburger-line {\r\n opacity: 0.75;\r\n}\r\n\r\n.hamburger-line {\r\n width: 30px;\r\n height: 2px;\r\n background-color: currentColor;\r\n transition:\r\n transform 0.25s ease,\r\n opacity 0.2s ease,\r\n margin 0.3s ease;\r\n transform-origin: 50% 50%;\r\n}\r\n\r\n.hamburger-menu.open .hamburger-line:first-child {\r\n transform: translateY(4px) rotate(45deg);\r\n}\r\n\r\n.hamburger-menu.open .hamburger-line:last-child {\r\n transform: translateY(-4px) rotate(-45deg);\r\n}\r\n\r\n.logo-container {\r\n display: flex;\r\n align-items: center;\r\n position: absolute;\r\n left: 50%;\r\n top: 50%;\r\n transform: translate(-50%, -50%);\r\n}\r\n\r\n.logo {\r\n height: 28px;\r\n}\r\n\r\n.card-nav-cta-button {\r\n background-color: #111;\r\n color: white;\r\n border: none;\r\n border-radius: calc(0.75rem - 0.35rem);\r\n padding: 0 1rem;\r\n height: 100%;\r\n font-weight: 500;\r\n cursor: pointer;\r\n transition: background-color 0.3s ease;\r\n}\r\n\r\n.card-nav-cta-button:hover {\r\n background-color: #333;\r\n}\r\n\r\n.card-nav-content {\r\n position: absolute;\r\n left: 0;\r\n right: 0;\r\n top: 60px;\r\n bottom: 0;\r\n padding: 0.5rem;\r\n display: flex;\r\n align-items: flex-end;\r\n gap: 12px;\r\n visibility: hidden;\r\n pointer-events: none;\r\n z-index: 1;\r\n}\r\n\r\n.card-nav.open .card-nav-content {\r\n visibility: visible;\r\n pointer-events: auto;\r\n}\r\n\r\n.nav-card {\r\n height: 100%;\r\n flex: 1 1 0;\r\n min-width: 0;\r\n border-radius: calc(0.75rem - 0.2rem);\r\n position: relative;\r\n display: flex;\r\n flex-direction: column;\r\n padding: 12px 16px;\r\n gap: 8px;\r\n user-select: none;\r\n}\r\n\r\n.nav-card-label {\r\n font-weight: 400;\r\n font-size: 22px;\r\n letter-spacing: -0.5px;\r\n}\r\n\r\n.nav-card-links {\r\n margin-top: auto;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n}\r\n\r\n.nav-card-link {\r\n font-size: 16px;\r\n cursor: pointer;\r\n text-decoration: none;\r\n transition: opacity 0.3s ease;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n}\r\n\r\n.nav-card-link:hover {\r\n opacity: 0.75;\r\n}\r\n\r\n@media (max-width: 768px) {\r\n .card-nav-container {\r\n width: 90%;\r\n top: 1.2em;\r\n }\r\n\r\n .card-nav-top {\r\n padding: 0.5rem 1rem;\r\n justify-content: space-between;\r\n }\r\n\r\n .hamburger-menu {\r\n order: 2;\r\n }\r\n\r\n .logo-container {\r\n position: static;\r\n transform: none;\r\n order: 1;\r\n }\r\n\r\n .card-nav-cta-button {\r\n display: none;\r\n }\r\n\r\n .card-nav-content {\r\n flex-direction: column;\r\n align-items: stretch;\r\n gap: 8px;\r\n padding: 0.5rem;\r\n bottom: 0;\r\n justify-content: flex-start;\r\n }\r\n\r\n .nav-card {\r\n height: auto;\r\n min-height: 60px;\r\n flex: 1 1 auto;\r\n max-height: none;\r\n }\r\n\r\n .nav-card-label {\r\n font-size: 18px;\r\n }\r\n\r\n .nav-card-link {\r\n font-size: 15px;\r\n }\r\n}\r\n", "type": "registry:item" } ] }