{ "$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';\nimport { gsap } from 'gsap';\n// use your own icon import if react-icons is not available\nimport { GoArrowUpRight } from 'react-icons/go';\nimport './CardNav.css';\n\nconst CardNav = ({\n logo,\n logoAlt = 'Logo',\n items,\n className = '',\n ease = 'power3.out',\n baseColor = '#fff',\n menuColor,\n buttonBgColor,\n buttonTextColor\n}) => {\n const [isHamburgerOpen, setIsHamburgerOpen] = useState(false);\n const [isExpanded, setIsExpanded] = useState(false);\n const navRef = useRef(null);\n const cardsRef = useRef([]);\n const tlRef = useRef(null);\n\n const calculateHeight = () => {\n const navEl = navRef.current;\n if (!navEl) return 260;\n\n const isMobile = window.matchMedia('(max-width: 768px)').matches;\n if (isMobile) {\n const contentEl = navEl.querySelector('.card-nav-content');\n if (contentEl) {\n const wasVisible = contentEl.style.visibility;\n const wasPointerEvents = contentEl.style.pointerEvents;\n const wasPosition = contentEl.style.position;\n const wasHeight = contentEl.style.height;\n\n contentEl.style.visibility = 'visible';\n contentEl.style.pointerEvents = 'auto';\n contentEl.style.position = 'static';\n contentEl.style.height = 'auto';\n\n contentEl.offsetHeight;\n\n const topBar = 60;\n const padding = 16;\n const contentHeight = contentEl.scrollHeight;\n\n contentEl.style.visibility = wasVisible;\n contentEl.style.pointerEvents = wasPointerEvents;\n contentEl.style.position = wasPosition;\n contentEl.style.height = wasHeight;\n\n return topBar + contentHeight + padding;\n }\n }\n return 260;\n };\n\n const createTimeline = () => {\n const navEl = navRef.current;\n if (!navEl) return null;\n\n gsap.set(navEl, { height: 60, overflow: 'hidden' });\n gsap.set(cardsRef.current, { y: 50, opacity: 0 });\n\n const tl = gsap.timeline({ paused: true });\n\n tl.to(navEl, {\n height: calculateHeight,\n duration: 0.4,\n ease\n });\n\n tl.to(cardsRef.current, { y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 }, '-=0.1');\n\n return tl;\n };\n\n useLayoutEffect(() => {\n const tl = createTimeline();\n tlRef.current = tl;\n\n return () => {\n tl?.kill();\n tlRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [ease, items]);\n\n useLayoutEffect(() => {\n const handleResize = () => {\n if (!tlRef.current) return;\n\n if (isExpanded) {\n const newHeight = calculateHeight();\n gsap.set(navRef.current, { height: newHeight });\n\n tlRef.current.kill();\n const newTl = createTimeline();\n if (newTl) {\n newTl.progress(1);\n tlRef.current = newTl;\n }\n } else {\n tlRef.current.kill();\n const newTl = createTimeline();\n if (newTl) {\n tlRef.current = newTl;\n }\n }\n };\n\n window.addEventListener('resize', handleResize);\n return () => window.removeEventListener('resize', handleResize);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isExpanded]);\n\n const toggleMenu = () => {\n const tl = tlRef.current;\n if (!tl) return;\n if (!isExpanded) {\n setIsHamburgerOpen(true);\n setIsExpanded(true);\n tl.play(0);\n } else {\n setIsHamburgerOpen(false);\n tl.eventCallback('onReverseComplete', () => setIsExpanded(false));\n tl.reverse();\n }\n };\n\n const setCardRef = i => el => {\n if (el) cardsRef.current[i] = el;\n };\n\n return (\n
\n \n
\n );\n};\n\nexport default CardNav;\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" } ] }