{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "navbar-corner-panel",
  "type": "registry:block",
  "title": "Navbar Corner Panel",
  "description": "A navbar that keeps its whole menu in one small frosted square in the corner. Opening it grows the square into a tall panel whose links drop in with their underlines drawing out, while the page dims behind and a lime close bubble follows the pointer.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/navbar-corner-panel.tsx",
      "type": "registry:component",
      "target": "components/tweenui/navbar-corner-panel.tsx",
      "content": "'use client';\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type MouseEvent,\n  type ReactNode,\n} from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nexport interface NavbarCornerPanelLink {\n  label: string;\n  href: string;\n}\n\nexport interface NavbarCornerPanelProps extends Omit<\n  ComponentPropsWithoutRef<'section'>,\n  'children'\n> {\n  brand?: string;\n  logo?: ReactNode;\n  homeHref?: string;\n  links?: NavbarCornerPanelLink[];\n  currentHref?: string;\n  socials?: NavbarCornerPanelLink[];\n  children?: ReactNode;\n}\n\ntype PanelSize = {\n  width: number;\n  height: number;\n};\n\nconst REPO = 'https://github.com/StaticMania/tween-ui';\n\nconst DEFAULT_LINKS: NavbarCornerPanelLink[] = [\n  { label: 'Home', href: '/' },\n  { label: 'Components', href: '/components' },\n  { label: 'Blocks', href: '/components#blocks' },\n  { label: 'Setup', href: '/installation/setup-guide' },\n];\n\nconst DEFAULT_SOCIALS: NavbarCornerPanelLink[] = [\n  { label: 'GitHub', href: REPO },\n  { label: 'Issues', href: `${REPO}/issues` },\n  { label: 'Changelog', href: `${REPO}/blob/main/CHANGELOG.md` },\n];\n\nconst TWEEN_MARK = (\n  <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" className=\"size-8\">\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';\nconst TOGGLE_SIZE = 44;\nconst GAP = 16;\nconst LARGE = 1024;\nconst MEDIUM = 768;\n\nconst EASE = 'ease-[cubic-bezier(0,0,0,1)]';\n\nconst focusRing =\n  'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#c6f56f]';\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 clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n\nconst ArrowIcon = ({ className }: { className?: string }) => {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 14 15\" fill=\"none\" className={className}>\n      <path\n        d=\"M3.06978 1.94975L12.9693 1.94973L12.9693 11.8492M0.948451 13.9705L12.6157 2.3033\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\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(40%_50%_at_96%_4%,rgb(198_245_111/0.14),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.08),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 text-sm text-[#9fd4d6]/70\">\n            <span aria-hidden=\"true\" className=\"text-[#c6f56f]\">\n              +\n            </span>\n            ({linkCount} pages, one corner)\n          </p>\n          <h2\n            id={headingId}\n            className=\"mt-5 text-[40px] leading-[0.98] font-medium tracking-[-0.045em] text-balance text-[#e8f3f2] sm:text-6xl md:text-7xl\"\n          >\n            Everything waits in the <span className=\"text-[#c6f56f]\">corner.</span>\n          </h2>\n          <p className=\"mx-auto mt-6 max-w-[34rem] text-[15px] leading-relaxed text-pretty text-[#e8f3f2]/60 sm:text-base md:text-lg\">\n            Open the square at the top right and it grows into the menu, straight out of its corner.\n            Click anywhere outside to put it away.\n          </p>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nconst NavbarCornerPanel = ({\n  brand = 'Tween UI',\n  logo = TWEEN_MARK,\n  homeHref = '/',\n  links = DEFAULT_LINKS,\n  currentHref = '/',\n  socials = DEFAULT_SOCIALS,\n  children,\n  className,\n  ...props\n}: NavbarCornerPanelProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [size, setSize] = useState<PanelSize>({ width: 0, height: 0 });\n  const menuId = useId();\n\n  const rootRef = useRef<HTMLElement>(null);\n  const anchorRef = useRef<HTMLDivElement>(null);\n  const toggleRef = useRef<HTMLButtonElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const overlayRef = useRef<HTMLButtonElement>(null);\n  const cursorRef = useRef<HTMLSpanElement>(null);\n  const isOpenRef = useRef(false);\n  const timelineRef = useRef<gsap.core.Timeline | null>(null);\n\n  const current = links.findIndex((link) => cleanHref(link.href) === cleanHref(currentHref));\n\n  const measure = useCallback((): PanelSize => {\n    const root = rootRef.current;\n    const anchor = anchorRef.current;\n    if (!root || !anchor) return { width: 0, height: 0 };\n    const rootBox = root.getBoundingClientRect();\n    const anchorBox = anchor.getBoundingClientRect();\n    const top = anchorBox.top - rootBox.top;\n    const reach = anchorBox.right - rootBox.left - GAP;\n    const rootWidth = rootBox.width;\n    const height = Math.round(rootBox.height - top * 2);\n\n    let width = reach;\n    if (rootWidth >= LARGE) width = clamp(rootWidth * 0.32, 340, 440);\n    else if (rootWidth >= MEDIUM) width = rootWidth * 0.48;\n    width = Math.round(Math.min(width, reach));\n\n    return { width, height };\n  }, []);\n\n  const { contextSafe } = useGSAP({ scope: rootRef });\n\n  const animatePanel = contextSafe((open: boolean, next: PanelSize) => {\n    const panel = panelRef.current;\n    const overlay = overlayRef.current;\n    if (!panel || !overlay) return;\n    const items = gsap.utils.toArray<HTMLElement>('[data-panel-item]', panel);\n    const extras = gsap.utils.toArray<HTMLElement>('[data-panel-extra]', panel);\n    const lines = gsap.utils.toArray<HTMLElement>('[data-panel-line]', panel);\n    timelineRef.current?.kill();\n\n    timelineRef.current = open\n      ? gsap\n          .timeline({ defaults: { duration: 0.5 } })\n          .set(panel, { visibility: 'visible' }, 0)\n          .set([...items, ...extras], { y: '-2rem', opacity: 0 }, 0)\n          .set(lines, { scaleX: 0 }, 0)\n          .fromTo(overlay, { autoAlpha: 0 }, { autoAlpha: 1 }, 0)\n          .to(panel, { width: next.width, height: next.height, ease: 'power1.out' }, 0)\n          .to(items, { y: 0, opacity: 1, stagger: 0.05 }, 0)\n          .to(lines, { scaleX: 1, stagger: 0.05, ease: 'power2.out' }, 0.15)\n          .to(extras, { y: 0, opacity: 1, stagger: 0.05 }, 0.2)\n      : gsap\n          .timeline({ defaults: { duration: 0.5 } })\n          .to([...items, ...extras], { y: '-2rem', opacity: 0, stagger: 0.03, duration: 0.3 }, 0)\n          .to(overlay, { autoAlpha: 0 }, 0)\n          .to(panel, { width: TOGGLE_SIZE, height: TOGGLE_SIZE, ease: 'power1.inOut' }, 0)\n          .set(panel, { visibility: 'hidden' });\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      const next = measure();\n      setSize(next);\n      setIsOpen(open);\n      animatePanel(open, next);\n    },\n    [animatePanel, measure]\n  );\n\n  const showCursor = contextSafe((isVisible: boolean) => {\n    gsap.to(cursorRef.current, {\n      autoAlpha: isVisible ? 1 : 0,\n      scale: isVisible ? 1 : 0.5,\n      duration: 0.25,\n      ease: 'power2.out',\n    });\n  });\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const cursor = cursorRef.current;\n    if (!isOpen || !root || !cursor) return;\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== 'Escape') return;\n      setOpen(false);\n      toggleRef.current?.focus();\n    };\n\n    const moveX = gsap.quickTo(cursor, 'x', { duration: 0.22, ease: 'power3.out' });\n    const moveY = gsap.quickTo(cursor, 'y', { duration: 0.22, ease: 'power3.out' });\n    const handlePointerMove = (event: PointerEvent) => {\n      if (event.pointerType === 'touch') {\n        showCursor(false);\n        return;\n      }\n      const box = root.getBoundingClientRect();\n      moveX(event.clientX - box.left);\n      moveY(event.clientY - box.top);\n      const target = event.target instanceof Element ? event.target : null;\n      showCursor(!target?.closest('[data-menu-keep]'));\n    };\n    const handlePointerLeave = () => showCursor(false);\n\n    const observer = new ResizeObserver(() => {\n      if (timelineRef.current?.isActive()) return;\n      const next = measure();\n      setSize(next);\n      gsap.set(panelRef.current, { width: next.width, height: next.height });\n    });\n\n    gsap.set(cursor, { xPercent: -50, yPercent: -50 });\n    document.addEventListener('keydown', handleKeyDown);\n    root.addEventListener('pointermove', handlePointerMove);\n    root.addEventListener('pointerleave', handlePointerLeave);\n    observer.observe(root);\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n      root.removeEventListener('pointermove', handlePointerMove);\n      root.removeEventListener('pointerleave', handlePointerLeave);\n      observer.disconnect();\n      showCursor(false);\n    };\n  }, [isOpen, measure, setOpen, showCursor]);\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      <div\n        inert={isOpen}\n        data-lenis-prevent=\"\"\n        className=\"absolute inset-0 z-0 [scrollbar-width:none] overflow-y-auto [&::-webkit-scrollbar]:hidden\"\n      >\n        {children ?? <SamplePage linkCount={links.length} />}\n      </div>\n\n      <button\n        ref={overlayRef}\n        type=\"button\"\n        tabIndex={-1}\n        aria-hidden=\"true\"\n        onClick={() => setOpen(false)}\n        className=\"invisible absolute inset-0 z-10 bg-[#03110f]/55 opacity-0 backdrop-blur-[2px] [@media(pointer:fine)]:cursor-none\"\n      />\n\n      <header className=\"pointer-events-none absolute inset-x-0 top-0 z-20\">\n        <div className={cn(CONTAINER, 'relative flex items-start justify-between pt-6 sm:pt-8')}>\n          <a\n            href={homeHref}\n            aria-label={`${brand} home`}\n            className={cn(\n              'pointer-events-auto flex items-center gap-2.5 rounded-lg text-[#e8f3f2]',\n              focusRing\n            )}\n          >\n            {logo}\n            <span className=\"text-lg font-semibold tracking-tight\">{brand}</span>\n          </a>\n\n          <div ref={anchorRef} data-menu-keep=\"\" className=\"pointer-events-auto relative size-11\">\n            <div\n              ref={panelRef}\n              id={menuId}\n              inert={!isOpen}\n              className=\"invisible absolute top-0 right-0 size-11 overflow-hidden rounded-lg bg-[#071d1b]/85 ring-1 ring-[#9fd4d6]/12 backdrop-blur-md\"\n            >\n              <div\n                style={{ width: size.width || undefined, height: size.height || undefined }}\n                className=\"absolute top-0 right-0\"\n              >\n                <nav\n                  aria-label=\"Primary\"\n                  className=\"@container flex h-full flex-col justify-between pt-16 pr-6 pb-8 pl-6 sm:pt-18 sm:pr-12 sm:pb-10 sm:pl-10\"\n                >\n                  <ul className=\"flex flex-col gap-4\">\n                    {links.map((link, index) => (\n                      <li key={`${link.label}-${link.href}`} data-panel-item=\"\">\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={cn(\n                            'group/link flex flex-col gap-3 rounded-sm text-4xl font-normal tracking-tight transition-colors duration-200 ease-out select-none hover:text-[#e8f3f2] sm:gap-4 @[22rem]:text-5xl',\n                            focusRing,\n                            index === current ? 'text-[#e8f3f2]' : 'text-[#9fd4d6]/45'\n                          )}\n                        >\n                          <span className=\"flex items-center justify-between gap-4\">\n                            <span className=\"min-w-0 origin-left truncate transition-transform duration-200 ease-out group-hover/link:scale-105\">\n                              {link.label}\n                            </span>\n                            <span className=\"block w-3.5 shrink-0 transition-transform duration-200 ease-out group-hover/link:translate-x-1\">\n                              <ArrowIcon\n                                className={cn(\n                                  'w-full',\n                                  index === current ? 'rotate-90 text-[#c6f56f]' : 'rotate-45'\n                                )}\n                              />\n                            </span>\n                          </span>\n                          <span\n                            aria-hidden=\"true\"\n                            data-panel-line=\"\"\n                            className=\"block h-px w-full origin-left bg-[#9fd4d6]/15\"\n                          />\n                        </a>\n                      </li>\n                    ))}\n                  </ul>\n                  {socials.length > 0 ? (\n                    <div className=\"flex w-fit flex-col gap-1 tracking-tight\">\n                      <p data-panel-extra=\"\" className=\"mb-4 text-[#9fd4d6]/70 select-none sm:mb-6\">\n                        Elsewhere\n                      </p>\n                      <ul className=\"flex flex-col gap-1\">\n                        {socials.map((social) => (\n                          <li key={social.href} data-panel-extra=\"\">\n                            <a\n                              href={social.href}\n                              {...externalProps(social.href)}\n                              className={cn(\n                                'group/social relative flex items-center gap-2 overflow-hidden rounded-xs pr-4 text-[#e8f3f2] sm:gap-0',\n                                focusRing\n                              )}\n                            >\n                              <span className=\"block w-2.5 origin-left transition-transform duration-200 ease-out sm:scale-0 sm:group-hover/social:scale-100\">\n                                <ArrowIcon className=\"w-full text-[#c6f56f]\" />\n                              </span>\n                              <span className=\"transition-transform duration-200 ease-out sm:-translate-x-2.5 sm:group-hover/social:translate-x-1\">\n                                {social.label}\n                              </span>\n                            </a>\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  ) : null}\n                </nav>\n              </div>\n            </div>\n\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={cn(\n                'absolute inset-0 z-20 grid cursor-pointer place-items-center rounded-lg backdrop-blur-md transition-colors duration-300',\n                isOpen ? 'bg-transparent' : 'bg-[#071d1b]/85 ring-1 ring-[#9fd4d6]/12',\n                focusRing\n              )}\n            >\n              <span aria-hidden=\"true\" className=\"relative block h-2.5 w-5\">\n                <span\n                  className={cn(\n                    'absolute inset-x-0 top-0 h-0.5 bg-[#e8f3f2] transition-transform duration-400 motion-reduce:transition-none',\n                    EASE,\n                    isOpen && 'translate-y-1 rotate-45'\n                  )}\n                />\n                <span\n                  className={cn(\n                    'absolute inset-x-0 bottom-0 h-0.5 bg-[#e8f3f2] transition-transform duration-400 motion-reduce:transition-none',\n                    EASE,\n                    isOpen && '-translate-y-1 -rotate-45'\n                  )}\n                />\n              </span>\n            </button>\n          </div>\n        </div>\n      </header>\n\n      <span\n        ref={cursorRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none invisible absolute top-0 left-0 z-30 grid size-14 place-items-center rounded-full bg-[#c6f56f] text-[#03110f] opacity-0\"\n      >\n        <svg\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          className=\"size-5\"\n        >\n          <path d=\"M18 6 6 18M6 6l12 12\" />\n        </svg>\n      </span>\n    </section>\n  );\n};\n\nexport default NavbarCornerPanel;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/block/navbar-corner-panel\nPass your own `links` and `socials` and put your page inside as `children`; the panel sizes itself to the screen it opens on."
}