{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "CardNav-TS-TW", "type": "registry:block", "title": "CardNav", "description": "Expandable navigation bar with card panels revealing nested links.", "dependencies": [ "gsap" ], "files": [ { "path": "public/ts/tailwind/src/ts-tailwind/Components/CardNav/CardNav.tsx", "content": "import React, { 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\n\r\ntype CardNavLink = {\r\n label: string;\r\n href: string;\r\n ariaLabel: string;\r\n};\r\n\r\nexport type CardNavItem = {\r\n label: string;\r\n bgColor: string;\r\n textColor: string;\r\n links: CardNavLink[];\r\n};\r\n\r\nexport interface CardNavProps {\r\n logo: string;\r\n logoAlt?: string;\r\n items: CardNavItem[];\r\n className?: string;\r\n ease?: string;\r\n baseColor?: string;\r\n menuColor?: string;\r\n buttonBgColor?: string;\r\n buttonTextColor?: string;\r\n}\r\n\r\nconst CardNav: React.FC = ({\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') as HTMLElement;\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 }, [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 }, [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: number) => (el: HTMLDivElement | null) => {\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\n
\r\n\r\n
\r\n {logoAlt}\r\n
\r\n\r\n