{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "curved-navbar",
  "type": "registry:ui",
  "title": "Curved Navbar",
  "description": "A curved, responsive navbar that smoothly expands into a three-column navigation panel on hover or click.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/curved-navbar.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type ReactNode } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { ArrowUpRight, Menu, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface CurvedNavbarItem {\n  label: string;\n  href: string;\n  description?: string;\n}\n\nexport interface CurvedNavbarProps {\n  brand?: string;\n  brandHref?: string;\n  logo?: ReactNode;\n  logoSrc?: string;\n  items?: readonly CurvedNavbarItem[];\n  actionLabel?: string;\n  actionHref?: string;\n  panelHeading?: string;\n  panelDescription?: string;\n  children?: ReactNode;\n  duration?: number;\n  stiffness?: number;\n  closeDelay?: number;\n  compactWidth?: number;\n  expandedWidth?: number;\n  curveRadius?: number;\n  background?: string;\n  foreground?: string;\n  accent?: string;\n  openOnHover?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n}\n\nexport const CURVED_NAVBAR_ITEMS: readonly CurvedNavbarItem[] = [\n  {\n    label: \"Features\",\n    href: \"#features\",\n    description: \"Built for your everyday flow.\",\n  },\n  {\n    label: \"Stories\",\n    href: \"#stories\",\n    description: \"A little inspiration, every day.\",\n  },\n  {\n    label: \"Updates\",\n    href: \"#updates\",\n    description: \"See what we have been working on.\",\n  },\n  {\n    label: \"About\",\n    href: \"#about\",\n    description: \"The people behind the details.\",\n  },\n];\n\nconst bounded = (value: number, min: number, max: number, fallback: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nexport function CurvedNavbar({\n  brand = \"devclub\",\n  brandHref = \"#home\",\n  logo,\n  logoSrc = \"https://i.pinimg.com/736x/16/16/72/16167292c79a7d3ca27ef4f94d1b7424.jpg\",\n  items = CURVED_NAVBAR_ITEMS,\n  actionLabel = \"Get started\",\n  actionHref = \"#get-started\",\n  panelHeading = \"A space for everything.\",\n  panelDescription = \"Explore the details. Find your next perspective.\",\n  children,\n  duration = 0.6,\n  stiffness = 0.35,\n  closeDelay = 0.12,\n  compactWidth = 880,\n  expandedWidth = 1040,\n  curveRadius = 26,\n  background = \"#080808\",\n  foreground = \"#f5f5f5\",\n  accent = \"#b9f582\",\n  openOnHover = true,\n  onOpenChange,\n  className,\n}: CurvedNavbarProps) {\n  const frameRef = useRef<HTMLDivElement>(null);\n  const shellRef = useRef<HTMLElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const timelineRef = useRef<gsap.core.Timeline | null>(null);\n  const closeRef = useRef<gsap.core.Tween | null>(null);\n  const animateRef = useRef<((expanded: boolean) => void) | null>(null);\n  const openRef = useRef(false);\n  const changeRef = useRef(onOpenChange);\n  const [open, setOpen] = useState(false);\n  const panelId = useId();\n  const radius = bounded(curveRadius, 12, 40, 26);\n  const speed = bounded(duration, 0.15, 1.5, 0.6);\n  const spring = bounded(stiffness, 0, 1, 0.35);\n  const delay = bounded(closeDelay, 0, 0.6, 0.12);\n  const narrowWidth = bounded(compactWidth, 320, 1400, 880);\n  const wideWidth = Math.max(\n    narrowWidth,\n    bounded(expandedWidth, 320, 1600, 1040),\n  );\n\n  useEffect(() => {\n    changeRef.current = onOpenChange;\n  }, [onOpenChange]);\n\n  const updateOpen = (next: boolean) => {\n    closeRef.current?.kill();\n    if (openRef.current === next) return;\n    openRef.current = next;\n    setOpen(next);\n    changeRef.current?.(next);\n  };\n\n  useGSAP(\n    () => {\n      const frame = frameRef.current;\n      const shell = shellRef.current;\n      const panel = panelRef.current;\n      const content = contentRef.current;\n      if (!frame || !shell || !panel || !content) return;\n      const media = gsap.matchMedia();\n      media.add(\n        { reduced: \"(prefers-reduced-motion: reduce)\", all: \"all\" },\n        (context) => {\n          const reduced = Boolean(context.conditions?.reduced);\n          let needsResize = false;\n          const build = context.add(\"build\", () => {\n            needsResize = false;\n            const wasOpen = openRef.current;\n            timelineRef.current?.kill();\n            const available = frame.clientWidth;\n            const collapsed = Math.min(\n              narrowWidth,\n              Math.max(0, available - 48),\n            );\n            const mobile = available < 680;\n            const viewportHeight = Math.max(\n              64,\n              frame.parentElement?.clientHeight ?? 0,\n            );\n            const fullHeight =\n              viewportHeight > 80 ? viewportHeight : window.innerHeight;\n            const expanded = mobile\n              ? available\n              : Math.min(wideWidth, Math.max(0, available - 32));\n            gsap.set(content, {\n              maxHeight: mobile\n                ? Math.max(0, fullHeight - 64)\n                : \"min(440px,65vh)\",\n            });\n            const entries = content.querySelectorAll(\"[data-navbar-entry]\");\n            gsap.set(shell, { width: expanded });\n            const expandedHeight = mobile\n              ? Math.max(0, fullHeight - 64)\n              : content.offsetHeight;\n            gsap.set(shell, {\n              width: collapsed,\n              top: 0,\n              borderBottomLeftRadius: radius,\n              borderBottomRightRadius: radius,\n            });\n            gsap.set(panel, { height: 0, autoAlpha: 0 });\n            gsap.set(entries, { y: reduced ? 0 : 12, opacity: 0 });\n            const timeline = gsap\n              .timeline({\n                paused: true,\n                onComplete: () => {\n                  if (needsResize) build();\n                },\n                onReverseComplete: () => {\n                  if (needsResize) build();\n                },\n              })\n              .to(\n                shell,\n                {\n                  width: expanded,\n                  top: mobile ? -frame.offsetTop : 0,\n                  borderBottomLeftRadius: mobile ? 0 : radius,\n                  borderBottomRightRadius: mobile ? 0 : radius,\n                  duration: reduced ? 0 : speed,\n                  ease:\n                    spring === 0 ? \"power3.out\" : `back.out(${spring * 0.8})`,\n                },\n                0,\n              )\n              .to(\n                panel,\n                {\n                  height: expandedHeight,\n                  autoAlpha: 1,\n                  duration: reduced ? 0 : speed,\n                  ease: \"power3.inOut\",\n                },\n                0,\n              )\n              .to(\n                entries,\n                {\n                  y: 0,\n                  opacity: 1,\n                  duration: reduced ? 0 : speed * 0.55,\n                  stagger: reduced ? 0 : Math.min(0.035, speed / 12),\n                  ease: \"power3.out\",\n                },\n                reduced ? 0 : speed * 0.22,\n              );\n            timelineRef.current = timeline;\n            if (reduced) animate(wasOpen);\n            else if (wasOpen) timeline.progress(1);\n          });\n          const animate = context.add(\"animate\", (next: boolean) => {\n            if (reduced) {\n              const available = frame.clientWidth;\n              const mobile = available < 680;\n              const viewportHeight = Math.max(\n                64,\n                frame.parentElement?.clientHeight ?? 0,\n              );\n              const fullHeight =\n                viewportHeight > 80 ? viewportHeight : window.innerHeight;\n              gsap.set(shell, {\n                width:\n                  next && mobile\n                    ? available\n                    : Math.min(\n                        next ? wideWidth : narrowWidth,\n                        Math.max(0, available - (next ? 32 : 48)),\n                      ),\n                top: next && mobile ? -frame.offsetTop : 0,\n                borderBottomLeftRadius: next && mobile ? 0 : radius,\n                borderBottomRightRadius: next && mobile ? 0 : radius,\n              });\n              gsap.set(panel, {\n                height: next\n                  ? mobile\n                    ? Math.max(0, fullHeight - 64)\n                    : content.offsetHeight\n                  : 0,\n                autoAlpha: next ? 1 : 0,\n              });\n              gsap.set(content.querySelectorAll(\"[data-navbar-entry]\"), {\n                y: 0,\n                opacity: next ? 1 : 0,\n              });\n            } else if (next) {\n              timelineRef.current?.play();\n            } else {\n              timelineRef.current?.reverse();\n            }\n          });\n          animateRef.current = (next) => animate(next);\n          const leave = context.add(\"leave\", (event: PointerEvent) => {\n            if (event.pointerType !== \"mouse\" || frame.clientWidth < 680)\n              return;\n            if (shell.contains(document.activeElement)) return;\n            closeRef.current?.kill();\n            closeRef.current = gsap.delayedCall(delay, () => updateOpen(false));\n          });\n          const handleLeave = (event: PointerEvent) => leave(event);\n          shell.addEventListener(\"pointerleave\", handleLeave);\n          build();\n          const observer = new ResizeObserver(() => {\n            const timeline = timelineRef.current;\n            if (timeline?.isActive()) {\n              needsResize = true;\n              return;\n            }\n            build();\n          });\n          observer.observe(frame);\n          if (frame.parentElement) observer.observe(frame.parentElement);\n          observer.observe(content);\n          return () => {\n            observer.disconnect();\n            shell.removeEventListener(\"pointerleave\", handleLeave);\n            closeRef.current?.kill();\n            timelineRef.current?.kill();\n            timelineRef.current = null;\n            animateRef.current = null;\n          };\n        },\n      );\n      return () => media.revert();\n    },\n    {\n      scope: frameRef,\n      dependencies: [\n        speed,\n        spring,\n        delay,\n        radius,\n        narrowWidth,\n        wideWidth,\n        items,\n        children,\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    () => {\n      closeRef.current?.kill();\n      animateRef.current?.(open);\n    },\n    { scope: frameRef, dependencies: [open] },\n  );\n\n  useEffect(() => {\n    if (!open) return;\n    const outside = (event: PointerEvent) => {\n      if (\n        event.target instanceof Node &&\n        !shellRef.current?.contains(event.target)\n      ) {\n        openRef.current = false;\n        setOpen(false);\n        changeRef.current?.(false);\n      }\n    };\n    document.addEventListener(\"pointerdown\", outside);\n    return () => document.removeEventListener(\"pointerdown\", outside);\n  }, [open]);\n\n  return (\n    <div\n      ref={frameRef}\n      data-slot=\"curved-navbar\"\n      className={cn(\"@container/nav relative z-20 h-16 w-full\", className)}\n    >\n      <nav\n        ref={shellRef}\n        aria-label={`${brand} navigation`}\n        data-open={open}\n        className=\"absolute left-1/2 top-0 w-[calc(100%-48px)] -translate-x-1/2 rounded-b-(--nav-radius) shadow-[0_16px_48px_-20px_rgba(0,0,0,0.6)]\"\n        style={\n          {\n            background,\n            color: foreground,\n            \"--nav-radius\": `${radius}px`,\n            \"--nav-accent\": accent,\n          } as React.CSSProperties\n        }\n        onPointerEnter={(event) => {\n          closeRef.current?.kill();\n          if (\n            openOnHover &&\n            event.pointerType === \"mouse\" &&\n            (frameRef.current?.clientWidth ?? 0) >= 680\n          )\n            updateOpen(true);\n        }}\n        onBlur={(event) => {\n          if (!event.currentTarget.contains(event.relatedTarget))\n            updateOpen(false);\n        }}\n        onKeyDown={(event) => {\n          if (event.key !== \"Escape\" || !open) return;\n          event.preventDefault();\n          event.stopPropagation();\n          updateOpen(false);\n          triggerRef.current?.focus();\n        }}\n      >\n        <svg\n          aria-hidden=\"true\"\n          viewBox=\"0 0 40 40\"\n          className=\"pointer-events-none absolute right-full top-0\"\n          style={{ width: radius, height: radius, fill: background }}\n        >\n          <path d=\"M0 0H40V40C40 18 22 0 0 0Z\" />\n        </svg>\n        <svg\n          aria-hidden=\"true\"\n          viewBox=\"0 0 40 40\"\n          className=\"pointer-events-none absolute left-full top-0 -scale-x-100\"\n          style={{ width: radius, height: radius, fill: background }}\n        >\n          <path d=\"M0 0H40V40C40 18 22 0 0 0Z\" />\n        </svg>\n        <div className=\"flex h-16 items-center gap-3 px-4 @[680px]/nav:px-5\">\n          <a\n            href={brandHref}\n            onClick={() => updateOpen(false)}\n            className=\"flex shrink-0 items-center gap-2.5 rounded-md font-semibold tracking-tight focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--nav-accent)\"\n          >\n            <span\n              aria-hidden=\"true\"\n              className=\"flex size-8 items-center justify-center rounded-[10px] bg-(--nav-accent) text-black\"\n            >\n              {logo ??\n                (logoSrc ? (\n                  <img\n                    src={logoSrc}\n                    alt=\"\"\n                    className=\"size-full rounded-[inherit] object-cover\"\n                  />\n                ) : (\n                  <span className=\"text-xl font-bold\">{brand.slice(0, 1)}</span>\n                ))}\n            </span>\n            <span className=\"max-w-28 truncate\">{brand}</span>\n          </a>\n          <div className=\"hidden min-w-0 flex-1 items-center justify-center gap-1 @[680px]/nav:flex\">\n            {items.slice(0, 6).map((item) => (\n              <a\n                key={`${item.label}-${item.href}`}\n                href={item.href}\n                onClick={() => updateOpen(false)}\n                className=\"truncate rounded-lg px-3 py-2 text-[13px] opacity-65 hover:bg-white/8 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-(--nav-accent)\"\n              >\n                {item.label}\n              </a>\n            ))}\n          </div>\n          <a\n            href={actionHref}\n            onClick={() => updateOpen(false)}\n            className=\"ml-auto hidden min-h-9 shrink-0 items-center gap-1.5 rounded-xl bg-(--nav-accent) px-3 text-xs font-semibold text-black focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--nav-accent) @[680px]/nav:flex @[680px]/nav:text-[13px]\"\n          >\n            {actionLabel}\n            <ArrowUpRight aria-hidden=\"true\" className=\"size-3.5\" />\n          </a>\n          <button\n            ref={triggerRef}\n            type=\"button\"\n            aria-label={open ? \"Close navigation\" : \"Open navigation\"}\n            aria-expanded={open}\n            aria-controls={panelId}\n            onClick={() => updateOpen(!openRef.current)}\n            className=\"ml-auto flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full bg-white/8 hover:bg-white/15 focus-visible:outline-2 focus-visible:outline-(--nav-accent) @[680px]/nav:sr-only @[680px]/nav:focus-visible:not-sr-only @[680px]/nav:focus-visible:absolute @[680px]/nav:focus-visible:right-5 @[680px]/nav:focus-visible:top-4 @[680px]/nav:focus-visible:h-8 @[680px]/nav:focus-visible:w-auto @[680px]/nav:focus-visible:bg-[#222] @[680px]/nav:focus-visible:px-3\"\n          >\n            <span className=\"@[680px]/nav:hidden\">\n              {open ? (\n                <X aria-hidden=\"true\" className=\"size-4\" />\n              ) : (\n                <Menu aria-hidden=\"true\" className=\"size-4\" />\n              )}\n            </span>\n            <span className=\"hidden text-xs @[680px]/nav:block\">\n              {open ? \"Close\" : \"Menu\"}\n            </span>\n          </button>\n        </div>\n        <div\n          ref={panelRef}\n          id={panelId}\n          aria-hidden={!open}\n          inert={!open}\n          className=\"h-0 overflow-hidden invisible\"\n        >\n          <div\n            ref={contentRef}\n            className=\"max-h-[min(440px,65vh)] overflow-y-auto overscroll-contain border-t border-white/10 scrollbar-thin [scrollbar-color:rgba(255,255,255,0.2)_transparent]\"\n          >\n            <div className=\"grid gap-6 p-5 @[680px]/nav:grid-cols-[1fr_1.1fr_1fr] @[680px]/nav:gap-8 @[680px]/nav:p-7\">\n              <div\n                data-navbar-entry\n                className=\"flex flex-col justify-between gap-5\"\n              >\n                <div>\n                  <span className=\"text-[10px] font-medium uppercase tracking-[0.2em] opacity-45\">\n                    Discover {brand}\n                  </span>\n                  <h2 className=\"mt-3 max-w-64 text-2xl font-medium leading-tight tracking-tight\">\n                    {panelHeading}\n                  </h2>\n                  <p className=\"mt-3 max-w-64 text-xs leading-relaxed opacity-50\">\n                    {panelDescription}\n                  </p>\n                </div>\n                <a\n                  href={actionHref}\n                  onClick={() => updateOpen(false)}\n                  className=\"flex w-fit items-center gap-2 rounded-md text-xs text-(--nav-accent) focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--nav-accent)\"\n                >\n                  {actionLabel}\n                  <ArrowUpRight aria-hidden=\"true\" className=\"size-3.5\" />\n                </a>\n              </div>\n              <div className=\"grid content-start gap-1\">\n                {items.map((item, index) => (\n                  <a\n                    data-navbar-entry\n                    key={`${item.label}-${item.href}`}\n                    href={item.href}\n                    onClick={() => updateOpen(false)}\n                    className=\"group flex items-center gap-3 rounded-xl px-3 py-2.5 hover:bg-white/6 focus-visible:outline-2 focus-visible:outline-(--nav-accent)]\"\n                  >\n                    <span\n                      aria-hidden=\"true\"\n                      className=\"text-[10px] tabular-nums opacity-30\"\n                    >\n                      {String(index + 1).padStart(2, \"0\")}\n                    </span>\n                    <span className=\"min-w-0 flex-1\">\n                      <span className=\"block text-sm font-medium\">\n                        {item.label}\n                      </span>\n                      {item.description && (\n                        <span className=\"mt-0.5 block text-[11px] leading-relaxed opacity-45\">\n                          {item.description}\n                        </span>\n                      )}\n                    </span>\n                    <ArrowUpRight\n                      aria-hidden=\"true\"\n                      className=\"size-3.5 shrink-0 opacity-25 group-hover:opacity-100\"\n                    />\n                  </a>\n                ))}\n              </div>\n              {children && (\n                <div data-navbar-entry className=\"min-w-0\">\n                  {children}\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      </nav>\n    </div>\n  );\n}\n\nexport default CurvedNavbar;\n",
      "type": "registry:ui",
      "target": "@ui/curved-navbar.tsx"
    }
  ]
}
