{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "slug": "curved-navbar",
  "name": "Curved Navbar",
  "title": "Curved Navbar",
  "type": "registry:ui",
  "description": "A curved, responsive navbar that smoothly expands into a three-column navigation panel on hover or click.",
  "summary": "Inverted outer curves connect the compact navigation bar to the page. A reversible GSAP timeline synchronizes width, panel height, and staggered content. Customize brand, links, featured content, colors, curve radius, timing, and spring response. On narrow screens the navbar expands to fill its page container, with aligned top controls and a scrollable panel.",
  "category": "navbar",
  "tags": [
    "navbar",
    "navigation",
    "curved",
    "hover",
    "responsive",
    "gsap"
  ],
  "dependencies": [
    "gsap",
    "@gsap/react",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-04",
  "updatedDate": "2026-10-04",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Inverted outer curves and aligned rounded lower corners",
    "Interruptible expansion and reversal with synchronized content",
    "Container-based compact navigation and stacked mobile panel",
    "Mouse, click, keyboard, outside press and Escape interactions",
    "Customizable links, branding, featured content, colors and motion",
    "Reduced-motion fallback and scoped animation cleanup",
    "Clean desktop bar without a dropdown icon; keyboard disclosure remains available",
    "Full screen mobile disclosure with a fixed header and scrollable content"
  ],
  "anatomy": [
    "Container-query wrapper and floating navigation shell",
    "Inverted SVG corner curves",
    "Brand, inline links, primary action and disclosure trigger",
    "Expandable panel with introduction, links and custom featured content"
  ],
  "physics": {
    "engine": "GSAP",
    "description": "A single reversible timeline animates width and panel height alongside staggered transforms and opacity. Stiffness controls a subtle back easing overshoot. Resize observation recalculates the container geometry. All timelines, delayed calls, observers and document listeners are cleaned up.",
    "parameters": [
      {
        "label": "Expansion",
        "value": "0.6s · power3.inOut"
      },
      {
        "label": "Spring response",
        "value": "0.35 · back.out(0.28)"
      },
      {
        "label": "Leave delay",
        "value": "0.12s"
      },
      {
        "label": "Curves",
        "value": "26px"
      }
    ]
  },
  "accessibility": {
    "role": "navigation",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Move through brand, links, action, disclosure and open panel links."
      },
      {
        "key": "Enter / Space",
        "description": "Toggle the navigation disclosure button."
      },
      {
        "key": "Escape",
        "description": "Close the panel and return focus to the disclosure trigger."
      }
    ],
    "aria": "Labeled navigation with aria-expanded and aria-controls on the trigger. The closed panel is inert and hidden from assistive technology. Focus leaving the navbar closes the disclosure. Narrow containers use click or keyboard activation instead of hover.",
    "reducedMotion": "Expansion and stagger animations resolve immediately when prefers-reduced-motion is enabled."
  },
  "guidelines": {
    "recommended": [
      "Landing pages with image or color backgrounds",
      "Compact desktop navigation with an expanded discovery panel",
      "Use the custom children slot for featured content"
    ],
    "bestPractices": [
      "Place inside a full-width page header with sufficient space below for the floating panel",
      "Use real destinations and short navigation labels",
      "Match the page top strip to the navbar background to connect the inverted curves",
      "Use a contrasting foreground and accent color",
      "Keep featured content and link descriptions concise"
    ]
  },
  "props": [
    {
      "name": "brand",
      "type": "string",
      "description": "Brand text displayed in the compact bar.",
      "defaultValue": "devclub"
    },
    {
      "name": "brandHref",
      "type": "string",
      "description": "Brand link destination.",
      "defaultValue": "#home"
    },
    {
      "name": "logo",
      "type": "ReactNode",
      "description": "Optional custom brand mark."
    },
    {
      "name": "logoSrc",
      "type": "string",
      "defaultValue": "https://i.pinimg.com/736x/16/16/72/16167292c79a7d3ca27ef4f94d1b7424.jpg",
      "description": "Brand image URL. Pass an empty string for the letter mark, or supply logo to override."
    },
    {
      "name": "items",
      "type": "readonly CurvedNavbarItem[]",
      "description": "Navigation labels, destinations and optional descriptions.",
      "defaultValue": "CURVED_NAVBAR_ITEMS"
    },
    {
      "name": "actionLabel",
      "type": "string",
      "description": "Primary action text.",
      "defaultValue": "Get started"
    },
    {
      "name": "actionHref",
      "type": "string",
      "description": "Primary action destination.",
      "defaultValue": "#get-started"
    },
    {
      "name": "panelHeading",
      "type": "string",
      "description": "Expanded panel heading.",
      "defaultValue": "A space for everything."
    },
    {
      "name": "panelDescription",
      "type": "string",
      "description": "Expanded panel supporting copy.",
      "defaultValue": "Explore the details. Find your next perspective."
    },
    {
      "name": "children",
      "type": "ReactNode",
      "description": "Custom featured content in the final panel column."
    },
    {
      "name": "duration",
      "type": "number",
      "description": "Expansion duration in seconds, bounded to 0.15–1.5.",
      "defaultValue": "0.6"
    },
    {
      "name": "stiffness",
      "type": "number",
      "description": "Spring response from 0 to 1. Higher values increase the restrained width overshoot.",
      "defaultValue": "0.35"
    },
    {
      "name": "closeDelay",
      "type": "number",
      "description": "Pointer leave grace period in seconds, bounded to 0–0.6.",
      "defaultValue": "0.12"
    },
    {
      "name": "compactWidth",
      "type": "number",
      "description": "Maximum collapsed width in pixels; adapts to the container.",
      "defaultValue": "880"
    },
    {
      "name": "expandedWidth",
      "type": "number",
      "description": "Maximum expanded width in pixels; never smaller than the collapsed width.",
      "defaultValue": "1040"
    },
    {
      "name": "curveRadius",
      "type": "number",
      "description": "Radius of the lower corners and inverted outer curves, bounded to 12–40.",
      "defaultValue": "26"
    },
    {
      "name": "background",
      "type": "string",
      "description": "Navbar and outer curve color.",
      "defaultValue": "#080808"
    },
    {
      "name": "foreground",
      "type": "string",
      "description": "Text color.",
      "defaultValue": "#f5f5f5"
    },
    {
      "name": "accent",
      "type": "string",
      "description": "Brand mark, action, and keyboard focus color.",
      "defaultValue": "#b9f582"
    },
    {
      "name": "openOnHover",
      "type": "boolean",
      "description": "Enable mouse hover expansion. Click and keyboard controls remain available.",
      "defaultValue": "true"
    },
    {
      "name": "onOpenChange",
      "type": "(open: boolean) => void",
      "description": "Called when the disclosure opens or closes."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional wrapper styles."
    }
  ],
  "usage": "import { CurvedNavbar } from \"@/components/ui/curved-navbar\";\n\nexport function CurvedNavbarDemo() {\n  return <CurvedNavbar brand=\"devclub\" actionLabel=\"Get started\" actionHref=\"/docs\" />;\n}",
  "files": [
    {
      "name": "curved-navbar.tsx",
      "path": "registry/ui/curved-navbar.tsx",
      "code": "\"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",
      "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"
    }
  ]
}
