{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "navbar-push-menu",
  "type": "registry:block",
  "title": "Navbar Push Menu",
  "description": "A navbar whose menu sits behind the page. Opening it pushes the page down into a rounded, inset card and wipes the link cards up one after another, each poster settling as it lands, while the toggle fills lime and its bars cross. The bar hides as you scroll down the page, returns on the way up and blurs what passes beneath it.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/navbar-push-menu.tsx",
      "type": "registry:component",
      "target": "components/tweenui/navbar-push-menu.tsx",
      "content": "'use client';\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type CSSProperties,\n  type MouseEvent,\n  type ReactNode,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { CustomEase } from 'gsap/CustomEase';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP, CustomEase);\nif (!CustomEase.get('navbar-push')) CustomEase.create('navbar-push', '0.5,0,0,1');\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface NavbarPushMenuLink {\n  label: string;\n  href: string;\n  image: string;\n}\n\nexport interface NavbarPushMenuProps extends Omit<ComponentPropsWithoutRef<'section'>, 'children'> {\n  brand?: string;\n  logo?: ReactNode;\n  homeHref?: string;\n  links?: NavbarPushMenuLink[];\n  currentHref?: string;\n  children?: ReactNode;\n}\n\nconst SITE = 'https://tween-ui.vercel.app';\nconst REPO = 'https://github.com/StaticMania/tween-ui';\n\nconst poster = (kind: 'block' | 'component', name: string) =>\n  `${SITE}/media/${kind}s/${name}-poster.webp`;\n\nconst DEFAULT_LINKS: NavbarPushMenuLink[] = [\n  { label: 'Home', href: '/', image: poster('block', 'gyro-gallery') },\n  { label: 'Components', href: '/components', image: poster('component', 'sliding-tabs') },\n  { label: 'Blocks', href: '/components#blocks', image: poster('block', 'shutter-slider') },\n  {\n    label: 'Changelog',\n    href: `${REPO}/blob/main/CHANGELOG.md`,\n    image: poster('block', 'kinetic-type-ring'),\n  },\n  {\n    label: 'Contribute',\n    href: `${REPO}/blob/main/CONTRIBUTING.md`,\n    image: poster('block', 'integration-hub'),\n  },\n  { label: 'GitHub', href: REPO, image: poster('block', 'footer-wordmark-rise') },\n];\n\nconst TWEEN_MARK = (\n  <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" className=\"size-7\">\n    <rect width=\"24\" height=\"24\" rx=\"6\" fill=\"#045f64\" />\n    <path\n      d=\"M5 19 C 9.5 8.9, 5 5, 19 5\"\n      fill=\"none\"\n      stroke=\"#c6f56f\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n    />\n    <circle cx=\"19\" cy=\"5\" r=\"1.8\" fill=\"#c6f56f\" />\n  </svg>\n);\n\nconst CONTAINER = 'mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8';\n\nconst SHOW_BLUR_AFTER = 80;\nconst SCROLL_TOLERANCE = 6;\nconst CARD_RADIUS = 16;\n\nconst EASE_OUT = 'ease-[cubic-bezier(0.22,1,0.36,1)]';\n\nconst focusRing =\n  'rounded-xs focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#c6f56f]';\n\nconst blurLayers = [0, 1, 2, 3, 4, 5].map((layer): CSSProperties => {\n  const reach = 100 - 14 * layer;\n  const mask = `linear-gradient(to bottom, #000 0%, #000 ${Math.max(0, reach - 30)}%, transparent ${reach}%)`;\n  const blur = `blur(${18 * 2 ** (layer - 5)}px)`;\n  return {\n    backdropFilter: blur,\n    WebkitBackdropFilter: blur,\n    maskImage: mask,\n    WebkitMaskImage: mask,\n  };\n});\n\nconst cleanHref = (href: string) => href.split(/[?#]/)[0]?.replace(/\\/+$/, '') || '/';\n\nconst isExternal = (href: string) => /^https?:/.test(href);\n\nconst externalProps = (href: string) =>\n  isExternal(href) ? { target: '_blank', rel: 'noopener noreferrer' } : {};\n\nconst RollLabel = ({ label }: { label: string }) => {\n  return (\n    <span aria-hidden=\"true\" className=\"block overflow-hidden leading-[1.25]\">\n      {Array.from(label).map((char, index) => (\n        <span\n          key={`${char}-${index}`}\n          className={cn(\n            'inline-block whitespace-pre [text-shadow:0_1.25em_currentColor]',\n            'transition-transform duration-500 group-hover/toggle:-translate-y-full group-focus-visible/toggle:-translate-y-full',\n            'ease-[cubic-bezier(0.25,0.1,0.25,1)]',\n            'motion-reduce:transition-none motion-reduce:group-hover/toggle:translate-y-0 motion-reduce:group-focus-visible/toggle:translate-y-0'\n          )}\n          style={{ transitionDelay: `${index * 40}ms` }}\n        >\n          {char}\n        </span>\n      ))}\n    </span>\n  );\n};\n\nconst ArrowIcon = () => {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 11.5 11.5\"\n      fill=\"none\"\n      className={cn(\n        'size-2.5 shrink-0 transition-transform duration-500 group-hover/card:rotate-45 group-focus-visible/card:rotate-45 motion-reduce:transition-none',\n        EASE_OUT\n      )}\n    >\n      <path\n        d=\"M0.75 10.75L10.75 0.75M10.75 8.25V0.75H3.25\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n};\n\nconst SamplePage = ({ linkCount }: { linkCount: number }) => {\n  const headingId = useId();\n\n  return (\n    <section\n      aria-labelledby={headingId}\n      className=\"relative isolate flex h-full min-h-full flex-col overflow-hidden pt-24 pb-10 sm:pt-28 sm:pb-14\"\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 -z-10 bg-[repeating-linear-gradient(90deg,rgb(159_212_214/0.06)_0_1px,transparent_1px_48px),repeating-linear-gradient(0deg,rgb(159_212_214/0.045)_0_1px,transparent_1px_48px)] [mask-image:radial-gradient(ellipse_75%_65%_at_50%_40%,black,transparent)]\"\n      />\n      <span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(38%_46%_at_88%_12%,rgb(198_245_111/0.13),transparent_70%),radial-gradient(55%_60%_at_8%_100%,rgb(4_95_100/0.6),transparent_72%),radial-gradient(40%_36%_at_45%_0%,rgb(159_212_214/0.1),transparent_70%)]\"\n      />\n      <div className={cn(CONTAINER, 'flex flex-1 items-center')}>\n        <div className=\"mx-auto max-w-4xl text-center\">\n          <p className=\"inline-flex items-center gap-2.5 rounded-full bg-white/[0.04] py-2 pr-4 pl-2.5 text-[10px] leading-none font-medium tracking-[0.2em] text-[#9fd4d6] uppercase ring-1 ring-white/10\">\n            <span\n              aria-hidden=\"true\"\n              className=\"size-2 shrink-0 rounded-full bg-[#c6f56f] shadow-[0_0_5px_#c6f56f,0_0_12px_#c6f56f]\"\n            />\n            {linkCount} pages behind one button\n          </p>\n          <h2\n            id={headingId}\n            className=\"mt-8 text-[40px] leading-[0.98] font-medium tracking-[-0.045em] text-balance text-[#e8f3f2] sm:text-6xl md:text-7xl lg:text-[80px]\"\n          >\n            The page steps back.\n            <br />\n            The <span className=\"text-[#c6f56f]\">menu</span> was always there.\n          </h2>\n          <p className=\"mx-auto mt-6 max-w-[34rem] text-[15px] leading-relaxed text-pretty text-[#e8f3f2]/60 sm:mt-8 sm:text-base md:text-lg\">\n            Press Menu at the top right. This page drops into a card and the links behind it wipe up\n            one after another. Click the card or press Escape to bring it back.\n          </p>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nconst NavbarPushMenu = ({\n  brand = 'Tween UI',\n  logo = TWEEN_MARK,\n  homeHref = '/',\n  links = DEFAULT_LINKS,\n  currentHref = '/',\n  children,\n  className,\n  ...props\n}: NavbarPushMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const menuId = useId();\n\n  const rootRef = useRef<HTMLElement>(null);\n  const headroomRef = useRef<HTMLDivElement>(null);\n  const blurRef = useRef<HTMLDivElement>(null);\n  const toggleRef = useRef<HTMLButtonElement>(null);\n  const circleRef = useRef<HTMLSpanElement>(null);\n  const barTopRef = useRef<HTMLSpanElement>(null);\n  const barBottomRef = useRef<HTMLSpanElement>(null);\n  const menuRef = useRef<HTMLDivElement>(null);\n  const listRef = useRef<HTMLUListElement>(null);\n  const pageRef = useRef<HTMLDivElement>(null);\n  const scrollerRef = useRef<HTMLDivElement>(null);\n  const timelineRef = useRef<gsap.core.Timeline | null>(null);\n  const isOpenRef = useRef(false);\n  const syncHeadroomRef = useRef<() => void>(() => {});\n\n  const current = links.findIndex((link) => cleanHref(link.href) === cleanHref(currentHref));\n\n  const measureCard = useCallback(() => {\n    const root = rootRef.current;\n    const list = listRef.current;\n    if (!root || !list) return { y: 0, scale: 1, radius: 0 };\n    const width = root.clientWidth;\n    const gutter = width < 640 ? 12 : 24;\n    const scale = 1 - (2 * gutter) / width;\n    const y = Math.round(\n      list.getBoundingClientRect().bottom - root.getBoundingClientRect().top + 32\n    );\n    return { y, scale, radius: CARD_RADIUS / scale };\n  }, []);\n\n  const { contextSafe } = useGSAP({ scope: rootRef });\n\n  const animate = contextSafe((open: boolean) => {\n    const menu = menuRef.current;\n    const page = pageRef.current;\n    if (!menu || !page) return;\n    const media = [...menu.querySelectorAll<HTMLElement>('[data-menu-media]')];\n    const images = [...menu.querySelectorAll<HTMLElement>('[data-menu-image]')];\n    const labels = [...menu.querySelectorAll<HTMLElement>('[data-menu-label]')];\n    const barTop = barTopRef.current;\n    const barBottom = barBottomRef.current;\n    timelineRef.current?.kill();\n\n    if (open) {\n      const card = measureCard();\n      gsap.set(menu, { visibility: 'visible' });\n      gsap.set(page, { transformOrigin: '50% 0%' });\n      timelineRef.current = gsap\n        .timeline({ defaults: { ease: 'navbar-push' } })\n        .to(page, { y: card.y, scale: card.scale, borderRadius: card.radius, duration: 1.2 }, 0)\n        .to(\n          circleRef.current,\n          {\n            backgroundColor: '#c6f56f',\n            borderColor: '#c6f56f',\n            color: '#03110f',\n            duration: 0.6,\n            ease: 'power2.out',\n          },\n          0.1\n        )\n        .to(barTop, { y: 3, rotate: 45, duration: 0.8 }, 0.1)\n        .to(barBottom, { y: -3, rotate: -45, duration: 0.8 }, 0.1)\n        .set([...media, ...labels], { autoAlpha: 1 }, 0)\n        .fromTo(\n          media,\n          { clipPath: 'inset(100% 0% 0% 0% round 8px)' },\n          { clipPath: 'inset(0% 0% 0% 0% round 8px)', duration: 1.1, stagger: 0.06 },\n          0.3\n        )\n        .fromTo(images, { scale: 1.3 }, { scale: 1, duration: 1.3, stagger: 0.06 }, 0.3)\n        .fromTo(labels, { yPercent: 110 }, { yPercent: 0, duration: 1, stagger: 0.06 }, 0.45);\n    } else {\n      timelineRef.current = gsap\n        .timeline({\n          defaults: { ease: 'navbar-push' },\n          onComplete: () => {\n            gsap.set(menu, { clearProps: 'visibility' });\n            gsap.set([...media, ...labels], { clearProps: 'opacity,visibility' });\n            gsap.set(page, { clearProps: 'transform,transformOrigin,borderRadius' });\n          },\n        })\n        .to([...media, ...labels], { autoAlpha: 0, duration: 0.35, ease: 'power1.out' }, 0)\n        .to(page, { y: 0, scale: 1, borderRadius: 0, duration: 1 }, 0)\n        .to(\n          circleRef.current,\n          {\n            backgroundColor: 'rgba(198,245,111,0)',\n            borderColor: 'rgba(232,243,242,0.24)',\n            color: '#e8f3f2',\n            duration: 0.5,\n            ease: 'power2.out',\n            clearProps: 'backgroundColor,borderColor,color',\n          },\n          0\n        )\n        .to([barTop, barBottom], { y: 0, rotate: 0, duration: 0.7 }, 0);\n    }\n\n    if (prefersReducedMotion()) timelineRef.current.progress(1);\n  });\n\n  const setOpen = useCallback(\n    (open: boolean) => {\n      if (open === isOpenRef.current) return;\n      isOpenRef.current = open;\n      setIsOpen(open);\n      animate(open);\n      syncHeadroomRef.current();\n    },\n    [animate]\n  );\n\n  useEffect(() => {\n    const scroller = scrollerRef.current;\n    const headroom = headroomRef.current;\n    const blur = blurRef.current;\n    if (!scroller || !headroom || !blur) return;\n    let lastY = scroller.scrollTop;\n    let isHidden = false;\n    let isQueued = false;\n\n    const sync = () => {\n      isQueued = false;\n      const y = scroller.scrollTop;\n      const delta = y - lastY;\n      const isNearTop = y <= SHOW_BLUR_AFTER;\n      if (isOpenRef.current || isNearTop) isHidden = false;\n      else if (Math.abs(delta) >= SCROLL_TOLERANCE) isHidden = delta > 0;\n      if (isOpenRef.current || isNearTop || Math.abs(delta) >= SCROLL_TOLERANCE) lastY = y;\n      headroom.style.transform = isHidden ? 'translateY(calc(-100% - 48px))' : '';\n      blur.style.opacity = !isOpenRef.current && !isNearTop ? '1' : '0';\n    };\n\n    const queue = () => {\n      if (isQueued) return;\n      isQueued = true;\n      requestAnimationFrame(sync);\n    };\n\n    syncHeadroomRef.current = queue;\n    scroller.addEventListener('scroll', queue, { passive: true });\n    sync();\n    return () => {\n      syncHeadroomRef.current = () => {};\n      scroller.removeEventListener('scroll', queue);\n    };\n  }, []);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const page = pageRef.current;\n    if (!isOpen || !root || !page) return;\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== 'Escape') return;\n      setOpen(false);\n      toggleRef.current?.focus();\n    };\n    const observer = new ResizeObserver(() => {\n      if (timelineRef.current?.isActive()) return;\n      const card = measureCard();\n      gsap.set(page, { y: card.y, scale: card.scale, borderRadius: card.radius });\n    });\n\n    document.addEventListener('keydown', handleKeyDown);\n    observer.observe(root);\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n      observer.disconnect();\n    };\n  }, [isOpen, measureCard, setOpen]);\n\n  const handleLinkClick = (event: MouseEvent<HTMLAnchorElement>, index: number) => {\n    if (index !== current) return;\n    event.preventDefault();\n    setOpen(false);\n  };\n\n  return (\n    <section\n      ref={rootRef}\n      aria-label={`${brand} site`}\n      className={cn(\n        'relative isolate h-svh min-h-[560px] w-full overflow-hidden bg-[#03110f] text-[#e8f3f2]',\n        className\n      )}\n      {...props}\n    >\n      <header className=\"pointer-events-none absolute inset-x-0 top-0 z-30\">\n        <div\n          ref={headroomRef}\n          className={cn('relative transition-transform duration-700', EASE_OUT)}\n        >\n          <div\n            ref={blurRef}\n            aria-hidden=\"true\"\n            className=\"absolute inset-x-0 top-0 -z-10 h-[calc(100%+40px)] opacity-0 transition-opacity duration-400\"\n          >\n            {blurLayers.map((style, index) => (\n              <div key={index} className=\"absolute inset-0\" style={style} />\n            ))}\n          </div>\n          <div className={cn(CONTAINER, 'flex items-center justify-between py-4 sm:py-6')}>\n            <a\n              href={homeHref}\n              className={cn('pointer-events-auto flex items-center gap-2.5', focusRing)}\n            >\n              {logo}\n              <span className=\"text-lg font-semibold tracking-tight text-[#e8f3f2]\">{brand}</span>\n            </a>\n            <button\n              ref={toggleRef}\n              type=\"button\"\n              aria-label={isOpen ? 'Close menu' : 'Open menu'}\n              aria-expanded={isOpen}\n              aria-controls={menuId}\n              onClick={() => setOpen(!isOpenRef.current)}\n              className=\"group/toggle pointer-events-auto flex cursor-pointer items-center gap-3 rounded-full text-sm tracking-[0.14em] text-[#e8f3f2] uppercase focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#c6f56f] sm:gap-4\"\n            >\n              <RollLabel label=\"Menu\" />\n              <span\n                ref={circleRef}\n                aria-hidden=\"true\"\n                className=\"relative block size-11 rounded-full border border-[#e8f3f2]/24\"\n              >\n                <span\n                  ref={barTopRef}\n                  className=\"absolute top-[18px] left-1/2 h-px w-5 -translate-x-1/2 bg-current\"\n                />\n                <span\n                  ref={barBottomRef}\n                  className=\"absolute top-[24px] left-1/2 h-px w-5 -translate-x-1/2 bg-current\"\n                />\n              </span>\n            </button>\n          </div>\n        </div>\n      </header>\n\n      <div\n        ref={menuRef}\n        id={menuId}\n        inert={!isOpen}\n        className=\"invisible absolute inset-x-0 top-0 z-0\"\n      >\n        <div aria-hidden=\"true\" className=\"h-19 sm:h-23\" />\n        <nav aria-label=\"Main\" className={cn(CONTAINER, 'pt-4 sm:pt-6')}>\n          <ul\n            ref={listRef}\n            className=\"group/list grid grid-cols-3 gap-x-2 gap-y-5 sm:gap-x-4 lg:grid-cols-6\"\n          >\n            {links.map((link, index) => (\n              <li key={`${link.label}-${link.href}`}>\n                <a\n                  href={link.href}\n                  {...externalProps(link.href)}\n                  aria-current={index === current ? 'page' : undefined}\n                  onClick={(event) => handleLinkClick(event, index)}\n                  className=\"group/card block rounded-lg transition-opacity duration-500 group-has-[a:hover]/list:opacity-50 hover:opacity-100! focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#c6f56f]\"\n                >\n                  <span\n                    data-menu-media=\"\"\n                    className=\"relative block aspect-[1.2] overflow-hidden rounded-lg bg-[#9fd4d6]/5\"\n                  >\n                    <img\n                      src={link.image}\n                      alt=\"\"\n                      draggable={false}\n                      loading=\"lazy\"\n                      data-menu-image=\"\"\n                      className={cn(\n                        'block size-full object-cover object-top transition-transform duration-700 group-hover/card:scale-105 motion-reduce:transition-none',\n                        EASE_OUT\n                      )}\n                    />\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"pointer-events-none absolute inset-0 rounded-lg border border-[#9fd4d6]/12\"\n                    />\n                  </span>\n                  <span className=\"mt-2.5 block overflow-hidden sm:mt-3\">\n                    <span\n                      data-menu-label=\"\"\n                      className={cn(\n                        'flex items-center justify-between gap-2 text-xs tracking-[0.14em] uppercase',\n                        index === current\n                          ? 'text-[#c6f56f]'\n                          : 'text-[#9fd4d6]/60 transition-colors duration-300 group-hover/card:text-[#e8f3f2] group-focus-visible/card:text-[#e8f3f2]'\n                      )}\n                    >\n                      <span className=\"truncate\">\n                        {String(index + 1).padStart(2, '0')} {'//'} {link.label}\n                      </span>\n                      <ArrowIcon />\n                    </span>\n                  </span>\n                </a>\n              </li>\n            ))}\n          </ul>\n        </nav>\n      </div>\n\n      <div\n        ref={pageRef}\n        className=\"absolute inset-0 z-10 overflow-hidden bg-[#071d1b] shadow-[0_-24px_60px_-20px_rgb(0_0_0/0.6)] ring-1 ring-[#9fd4d6]/10 will-change-transform\"\n      >\n        <div\n          ref={scrollerRef}\n          inert={isOpen}\n          data-lenis-prevent=\"\"\n          className=\"h-full [scrollbar-width:none] overflow-y-auto [&::-webkit-scrollbar]:hidden\"\n        >\n          {children ?? <SamplePage linkCount={links.length} />}\n        </div>\n        {isOpen ? (\n          <button\n            type=\"button\"\n            tabIndex={-1}\n            aria-hidden=\"true\"\n            onClick={() => setOpen(false)}\n            className=\"absolute inset-0 z-10 cursor-pointer\"\n          />\n        ) : null}\n      </div>\n    </section>\n  );\n};\n\nexport default NavbarPushMenu;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/navbar-push-menu\nGSAP plugins (CustomEase) are registered inside the component, so there is nothing to set up.\nPass your own `links` and put your page inside as `children`; the menu band grows to fit however many cards you give it."
}