{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "curtain-navbar",
  "type": "registry:ui",
  "title": "Curtain Navbar",
  "description": "A floating glass header opens into a full-screen color curtain with oversized condensed links and synchronized hover details.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "@radix-ui/react-dialog",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.devclubxnst.com/r/ascii-hover-button.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/curtain-navbar.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\";\nimport * as Dialog from \"@radix-ui/react-dialog\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  AsciiGlitchText,\n  type AsciiGlitchTextHandle,\n} from \"./ascii-hover-button\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface CurtainNavbarItem {\n  label: string;\n  href: string;\n  category?: string;\n  detail?: string;\n  description?: string;\n}\n\nexport interface CurtainNavbarLink {\n  label: string;\n  href: string;\n}\n\nexport interface CurtainNavbarProps {\n  brand?: string;\n  brandHref?: string;\n  logo?: ReactNode;\n  items?: readonly CurtainNavbarItem[];\n  quickLinks?: readonly CurtainNavbarLink[];\n  footerLinks?: readonly CurtainNavbarLink[];\n  leftNote?: string;\n  rightNote?: string;\n  footerText?: string;\n  background?: string;\n  foreground?: string;\n  headerBackground?: string;\n  headerForeground?: string;\n  duration?: number;\n  stagger?: number;\n  hoverDuration?: number;\n  closeSpeed?: number;\n  glitchDuration?: number;\n  hoverScroll?: boolean;\n  scrollDuration?: number;\n  contained?: boolean;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n}\n\nexport const CURTAIN_NAVBAR_ITEMS: readonly CurtainNavbarItem[] = [\n  {\n    label: \"Build\",\n    href: \"/components\",\n    category: \"Components\",\n    detail: \"UI\",\n    description: \"Find your next component.\",\n  },\n  {\n    label: \"Learn\",\n    href: \"/docs/introduction\",\n    category: \"Documentation\",\n    detail: \"01\",\n    description: \"Make motion part of your craft.\",\n  },\n  {\n    label: \"Club\",\n    href: \"https://github.com/devclub-nstru/component_library\",\n    category: \"Community\",\n    detail: \"Open\",\n    description: \"Built together. Shared with everyone.\",\n  },\n  {\n    label: \"Hello\",\n    href: \"https://github.com/devclub-nstru/component_library/issues\",\n    category: \"Connect\",\n    detail: \"↗\",\n    description: \"Bring an idea. Start a conversation.\",\n  },\n  {\n    label: \"Source\",\n    href: \"https://github.com/devclub-nstru/component_library\",\n    category: \"Open source\",\n    detail: \"MIT\",\n    description: \"Explore the code behind the motion.\",\n  },\n];\n\nconst DEFAULT_QUICK_LINKS: readonly CurtainNavbarLink[] = [\n  { label: \"Build\", href: \"/components\" },\n  { label: \"Learn\", href: \"/docs/introduction\" },\n  {\n    label: \"Hello\",\n    href: \"https://github.com/devclub-nstru/component_library/issues\",\n  },\n];\n\nconst DEFAULT_FOOTER_LINKS: readonly CurtainNavbarLink[] = [\n  {\n    label: \"GitHub\",\n    href: \"https://github.com/devclub-nstru/component_library\",\n  },\n  { label: \"Installation\", href: \"/docs/installation\" },\n  { label: \"Dev Club\", href: \"https://devclub.co\" },\n];\n\nconst clamp = (value: number, min: number, max: number, fallback: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nfunction MenuMark({ open }: { open: boolean }) {\n  const rootRef = useRef<HTMLSpanElement>(null);\n  const animateRef = useRef<((next: boolean) => void) | null>(null);\n  const openRef = useRef(open);\n  useEffect(() => {\n    openRef.current = open;\n  }, [open]);\n  useGSAP(\n    () => {\n      const media = gsap.matchMedia();\n      media.add(\n        { reduced: \"(prefers-reduced-motion: reduce)\", all: \"all\" },\n        (context) => {\n          const animate = context.add(\"animate\", (next: boolean) =>\n            gsap.to(rootRef.current, {\n              rotation: next ? 135 : 0,\n              duration: context.conditions?.reduced ? 0 : 0.45,\n              ease: \"power3.inOut\",\n              overwrite: \"auto\",\n            }),\n          );\n          animateRef.current = (next) => animate(next);\n          animate(openRef.current);\n          return () => {\n            animateRef.current = null;\n          };\n        },\n      );\n      return () => media.revert();\n    },\n    { scope: rootRef },\n  );\n  useGSAP(\n    () => {\n      animateRef.current?.(open);\n    },\n    { scope: rootRef, dependencies: [open] },\n  );\n  return (\n    <span\n      ref={rootRef}\n      aria-hidden=\"true\"\n      className=\"grid size-3.5 grid-cols-2 gap-0.5\"\n    >\n      {[0, 1, 2, 3].map((index) => (\n        <span key={index} className=\"rounded-[1px] bg-current\" />\n      ))}\n    </span>\n  );\n}\n\nfunction HeaderLink({\n  link,\n  onNavigate,\n  duration,\n}: {\n  link: CurtainNavbarLink;\n  onNavigate?: () => void;\n  duration: number;\n}) {\n  const glitchRef = useRef<AsciiGlitchTextHandle>(null);\n  return (\n    <a\n      href={link.href}\n      aria-label={link.label}\n      onClick={onNavigate}\n      onPointerEnter={(event) => {\n        if (event.pointerType === \"mouse\") glitchRef.current?.trigger();\n      }}\n      onPointerLeave={() => glitchRef.current?.reset()}\n      onFocus={() => glitchRef.current?.trigger()}\n      onBlur={() => glitchRef.current?.reset()}\n      className=\"rounded-sm opacity-70 hover:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-current\"\n    >\n      <AsciiGlitchText\n        ref={glitchRef}\n        text={link.label.toUpperCase()}\n        duration={duration}\n        className=\"font-mono text-[10px] font-normal tracking-wide text-inherit transition-none [&>span]:w-[1ch] [&>span]:font-mono [&>span]:text-inherit [&>span]:transition-none\"\n      />\n    </a>\n  );\n}\n\nfunction CurtainHeader({\n  brand,\n  brandHref,\n  logo,\n  quickLinks,\n  button,\n  onNavigate,\n  background,\n  foreground,\n  glitchDuration,\n}: {\n  brand: string;\n  brandHref: string;\n  logo?: ReactNode;\n  quickLinks: readonly CurtainNavbarLink[];\n  button: ReactNode;\n  onNavigate?: () => void;\n  background: string;\n  foreground: string;\n  glitchDuration: number;\n}) {\n  return (\n    <header\n      className=\"@container/curtain-header pointer-events-auto absolute inset-x-4 top-4 z-20 mx-auto flex h-14 max-w-2xl items-center justify-between gap-3 rounded-xl px-4 font-mono text-[10px] uppercase tracking-wide backdrop-blur-xl\"\n      style={{ background, color: foreground }}\n    >\n      <a\n        href={brandHref}\n        onClick={onNavigate}\n        className=\"flex min-w-0 items-center gap-2 rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-current\"\n      >\n        {logo && (\n          <span className=\"flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-md\">\n            {logo}\n          </span>\n        )}\n        <span className=\"truncate\">{brand}</span>\n      </a>\n      <div className=\"flex shrink-0 items-center gap-2\">\n        <nav\n          aria-label=\"Quick navigation\"\n          className=\"hidden items-center gap-4 @[450px]/curtain-header:flex\"\n        >\n          {quickLinks.map((link) => (\n            <HeaderLink\n              key={`${link.label}-${link.href}`}\n              link={link}\n              onNavigate={onNavigate}\n              duration={glitchDuration}\n            />\n          ))}\n        </nav>\n        {button}\n      </div>\n    </header>\n  );\n}\n\ntype CurtainSettings = Required<\n  Omit<\n    CurtainNavbarProps,\n    \"logo\" | \"open\" | \"defaultOpen\" | \"onOpenChange\" | \"className\"\n  >\n> &\n  Pick<CurtainNavbarProps, \"logo\">;\n\nfunction CurtainPanel({\n  open,\n  onClose,\n  onExit,\n  settings,\n}: {\n  open: boolean;\n  onClose: () => void;\n  onExit: () => void;\n  settings: CurtainSettings;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const sheetRef = useRef<HTMLDivElement>(null);\n  const closeRef = useRef<HTMLButtonElement>(null);\n  const moveRef = useRef<((next: boolean) => void) | null>(null);\n  const openRef = useRef(open);\n  const enteredRef = useRef(false);\n  const {\n    brand,\n    brandHref,\n    logo,\n    items,\n    footerLinks,\n    leftNote,\n    rightNote,\n    footerText,\n    background,\n    foreground,\n    headerBackground,\n    headerForeground,\n    duration,\n    stagger,\n    hoverDuration,\n    closeSpeed,\n    glitchDuration,\n    hoverScroll,\n    scrollDuration,\n    contained,\n  } = settings;\n\n  useEffect(() => {\n    openRef.current = open;\n  }, [open]);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      const sheet = sheetRef.current;\n      if (!root || !sheet) return;\n      const titles = root.querySelectorAll<HTMLElement>(\"[data-curtain-title]\");\n      const notes = root.querySelectorAll<HTMLElement>(\"[data-curtain-note]\");\n      const nav = root.querySelector<HTMLElement>(\"[data-curtain-nav]\");\n      const scroller = root.querySelector<HTMLElement>(\"[data-curtain-scroll]\");\n      const media = gsap.matchMedia();\n      media.add(\n        {\n          reduced: \"(prefers-reduced-motion: reduce)\",\n          pointer: \"(hover: hover) and (pointer: fine)\",\n          all: \"all\",\n        },\n        (context) => {\n          const reduced = Boolean(context.conditions?.reduced);\n          gsap.set(sheet, {\n            y: 0,\n            yPercent: 100,\n            borderTopLeftRadius: 32,\n            borderTopRightRadius: 32,\n          });\n          gsap.set(titles, { y: 0, yPercent: 115 });\n          gsap.set(notes, { opacity: 0, y: 10 });\n          const timeline = gsap\n            .timeline({\n              paused: true,\n              onComplete: () => {\n                enteredRef.current = true;\n              },\n              onReverseComplete: onExit,\n            })\n            .to(\n              sheet,\n              {\n                yPercent: 0,\n                borderTopLeftRadius: 0,\n                borderTopRightRadius: 0,\n                duration,\n                ease: \"power4.inOut\",\n              },\n              0,\n            )\n            .to(\n              titles,\n              {\n                y: 0,\n                yPercent: 0,\n                duration: duration * 0.75,\n                stagger,\n                ease: \"power4.out\",\n              },\n              duration * 0.3,\n            )\n            .to(\n              notes,\n              {\n                opacity: 1,\n                y: 0,\n                duration: duration * 0.4,\n                stagger: 0.02,\n                ease: \"power3.out\",\n              },\n              duration * 0.6,\n            );\n          let selected: HTMLElement | null = null;\n          let pointer: { x: number; y: number } | null = null;\n          const scrollTo =\n            scroller && !reduced\n              ? gsap.quickTo(scroller, \"scrollTop\", {\n                  duration: scrollDuration,\n                  ease: \"power3.out\",\n                })\n              : null;\n          const select = context.add(\"select\", (row: HTMLElement | null) => {\n            if (selected === row) return;\n            if (selected) {\n              gsap.to(selected.querySelector(\"[data-curtain-word]\"), {\n                opacity: 1,\n                x: 0,\n                duration: reduced ? 0 : hoverDuration,\n                ease: \"power3.out\",\n                overwrite: \"auto\",\n              });\n              gsap.to(selected.querySelectorAll(\"[data-curtain-detail]\"), {\n                autoAlpha: 0,\n                y: reduced ? 0 : -8,\n                duration: reduced ? 0 : hoverDuration * 0.7,\n                ease: \"power2.out\",\n                overwrite: \"auto\",\n              });\n            }\n            selected = row;\n            if (!row) return;\n            gsap.to(row.querySelector(\"[data-curtain-word]\"), {\n              opacity: 0.45,\n              x: reduced ? 0 : 5,\n              duration: reduced ? 0 : hoverDuration,\n              ease: \"power3.out\",\n              overwrite: \"auto\",\n            });\n            gsap.fromTo(\n              row.querySelectorAll(\"[data-curtain-detail]\"),\n              { autoAlpha: 0, y: reduced ? 0 : 8 },\n              {\n                autoAlpha: 1,\n                y: 0,\n                duration: reduced ? 0 : hoverDuration,\n                ease: \"power3.out\",\n                overwrite: \"auto\",\n              },\n            );\n          });\n          const point = (event: Event) => {\n            if (event instanceof PointerEvent && event.pointerType !== \"mouse\")\n              return;\n            const row =\n              event.target instanceof Element\n                ? event.target.closest<HTMLElement>(\"[data-curtain-row]\")\n                : null;\n            if (row && nav?.contains(row)) select(row);\n          };\n          const scroll = (event: PointerEvent) => {\n            if (event.pointerType === \"mouse\")\n              pointer = { x: event.clientX, y: event.clientY };\n            if (\n              !scroller ||\n              !scrollTo ||\n              !hoverScroll ||\n              !context.conditions?.pointer ||\n              event.pointerType !== \"mouse\" ||\n              !openRef.current ||\n              !enteredRef.current\n            )\n              return;\n            const bounds = scroller.getBoundingClientRect();\n            const progress = gsap.utils.clamp(\n              0,\n              1,\n              (event.clientY - bounds.top - 24) /\n                Math.max(1, bounds.height - 48),\n            );\n            scrollTo(\n              progress *\n                Math.max(0, scroller.scrollHeight - scroller.clientHeight),\n              scroller.scrollTop,\n            );\n          };\n          const stopScroll = () => scrollTo?.tween.pause();\n          const syncHover = () => {\n            if (!pointer || nav?.contains(document.activeElement)) return;\n            const target = document.elementFromPoint(pointer.x, pointer.y);\n            const row =\n              target?.closest<HTMLElement>(\"[data-curtain-row]\") ?? null;\n            select(row && nav?.contains(row) ? row : null);\n          };\n          const leaveScroll = () => {\n            pointer = null;\n            stopScroll();\n          };\n          const focusScroll = (event: FocusEvent) => {\n            if (!scroller || !(event.target instanceof HTMLElement)) return;\n            stopScroll();\n            const bounds = scroller.getBoundingClientRect();\n            const target = event.target.getBoundingClientRect();\n            const offset =\n              target.top < bounds.top + 12\n                ? target.top - bounds.top - 12\n                : target.bottom > bounds.bottom - 12\n                  ? target.bottom - bounds.bottom + 12\n                  : 0;\n            if (offset === 0) return;\n            const next = gsap.utils.clamp(\n              0,\n              Math.max(0, scroller.scrollHeight - scroller.clientHeight),\n              scroller.scrollTop + offset,\n            );\n            if (scrollTo) scrollTo(next, scroller.scrollTop);\n            else scroller.scrollTop = next;\n          };\n          const leave = () => {\n            if (!nav?.contains(document.activeElement)) select(null);\n          };\n          const blur = (event: FocusEvent) => {\n            if (\n              !(event.relatedTarget instanceof Node) ||\n              !nav?.contains(event.relatedTarget)\n            )\n              select(null);\n          };\n          nav?.addEventListener(\"pointerover\", point);\n          nav?.addEventListener(\"focusin\", point);\n          nav?.addEventListener(\"pointerleave\", leave);\n          nav?.addEventListener(\"focusout\", blur);\n          scroller?.addEventListener(\"pointermove\", scroll, { passive: true });\n          scroller?.addEventListener(\"pointerleave\", leaveScroll);\n          scroller?.addEventListener(\"scroll\", syncHover, { passive: true });\n          scroller?.addEventListener(\"wheel\", stopScroll, { passive: true });\n          scroller?.addEventListener(\"touchstart\", stopScroll, {\n            passive: true,\n          });\n          scroller?.addEventListener(\"focusin\", focusScroll);\n          const move = context.add(\"move\", (next: boolean) => {\n            if (!next) stopScroll();\n            if (reduced) {\n              timeline.progress(1).pause();\n              if (!next) onExit();\n            } else if (next) timeline.timeScale(1).play();\n            else if (timeline.progress() === 0) onExit();\n            else timeline.timeScale(closeSpeed).reverse();\n          });\n          moveRef.current = (next) => move(next);\n          if (enteredRef.current && openRef.current)\n            timeline.progress(1).pause();\n          else move(openRef.current);\n          return () => {\n            moveRef.current = null;\n            nav?.removeEventListener(\"pointerover\", point);\n            nav?.removeEventListener(\"focusin\", point);\n            nav?.removeEventListener(\"pointerleave\", leave);\n            nav?.removeEventListener(\"focusout\", blur);\n            scroller?.removeEventListener(\"pointermove\", scroll);\n            scroller?.removeEventListener(\"pointerleave\", leaveScroll);\n            scroller?.removeEventListener(\"scroll\", syncHover);\n            scroller?.removeEventListener(\"wheel\", stopScroll);\n            scroller?.removeEventListener(\"touchstart\", stopScroll);\n            scroller?.removeEventListener(\"focusin\", focusScroll);\n          };\n        },\n      );\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        duration,\n        stagger,\n        hoverDuration,\n        closeSpeed,\n        hoverScroll,\n        scrollDuration,\n        items,\n        onExit,\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    () => {\n      moveRef.current?.(open);\n    },\n    { scope: rootRef, dependencies: [open] },\n  );\n\n  return (\n    <Dialog.Content\n      ref={rootRef}\n      data-slot=\"curtain-navbar-panel\"\n      className={cn(\n        \"@container/curtain pointer-events-auto inset-0 z-50 overflow-hidden outline-none\",\n        contained ? \"absolute\" : \"fixed\",\n      )}\n      style={{ color: foreground, containerType: \"size\" }}\n      onOpenAutoFocus={(event) => {\n        event.preventDefault();\n        closeRef.current?.focus({ preventScroll: true });\n      }}\n      onEscapeKeyDown={(event) => {\n        event.preventDefault();\n        onClose();\n      }}\n      onPointerDownOutside={(event) => event.preventDefault()}\n    >\n      <Dialog.Title className=\"sr-only\">{brand} navigation</Dialog.Title>\n      <Dialog.Description className=\"sr-only\">\n        Explore Dev Club components, documentation, and community resources.\n      </Dialog.Description>\n      <div\n        ref={sheetRef}\n        className=\"absolute inset-0 overflow-hidden\"\n        style={{ background }}\n      >\n        <div\n          data-curtain-note\n          className=\"pointer-events-none absolute left-7 top-5 hidden max-w-[16%] whitespace-pre-line font-mono text-[9px] uppercase leading-relaxed @[1100px]/curtain:block\"\n        >\n          {leftNote}\n        </div>\n        <div\n          data-curtain-note\n          className=\"pointer-events-none absolute right-7 top-5 hidden max-w-[16%] whitespace-pre-line text-right font-mono text-[9px] uppercase leading-relaxed @[1100px]/curtain:block\"\n        >\n          {rightNote}\n        </div>\n        <div\n          data-curtain-scroll\n          className=\"absolute inset-x-0 bottom-20 top-20 overflow-y-auto overscroll-contain px-4 py-3 scrollbar-none [&::-webkit-scrollbar]:hidden @[760px]/curtain:px-7\"\n        >\n          <nav\n            data-curtain-nav\n            aria-label={`${brand} main navigation`}\n            className=\"my-auto w-full shrink-0\"\n            style={\n              {\n                \"--curtain-row-height\": \"clamp(96px,20cqh,260px)\",\n              } as CSSProperties\n            }\n          >\n            {items.map((item, index) => (\n              <div\n                key={`${item.label}-${index}`}\n                data-curtain-row\n                className=\"relative flex items-center justify-center\"\n                style={{ height: \"var(--curtain-row-height)\" }}\n              >\n                <div\n                  data-curtain-detail\n                  aria-hidden=\"true\"\n                  className=\"pointer-events-none invisible absolute inset-y-0 left-0 hidden w-[22%] items-center justify-between gap-3 font-mono text-[9px] uppercase opacity-0 @[760px]/curtain:flex\"\n                >\n                  <span className=\"max-w-[75%] wrap-break-words\">\n                    {item.category}\n                  </span>\n                  <span>{item.detail}</span>\n                </div>\n                <a\n                  href={item.href}\n                  onClick={onClose}\n                  className=\"flex h-full w-full items-center justify-center overflow-hidden rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current @[760px]/curtain:w-[54%]\"\n                  aria-label={item.label}\n                >\n                  <span\n                    className=\"block shrink-0 pb-1\"\n                    style={{\n                      clipPath: \"inset(0 -100% 0 -100%)\",\n                      fontFamily: \"Arial, Helvetica, sans-serif\",\n                      fontSize: `clamp(60px,min(calc(var(--curtain-row-height) * 1.05),${Math.min(34, 235 / Math.max(item.label.length, 1))}cqw),260px)`,\n                      fontWeight: 400,\n                      lineHeight: 0.9,\n                      letterSpacing: \"-0.075em\",\n                    }}\n                  >\n                    <span data-curtain-title className=\"block\">\n                      <span\n                        data-curtain-word\n                        className=\"block origin-center scale-x-[0.62] whitespace-nowrap uppercase\"\n                      >\n                        {item.label}\n                      </span>\n                    </span>\n                  </span>\n                </a>\n                <div\n                  data-curtain-detail\n                  aria-hidden=\"true\"\n                  className=\"pointer-events-none invisible absolute inset-y-0 right-0 hidden w-[22%] items-center justify-end text-right font-mono text-[9px] uppercase leading-relaxed opacity-0 @[760px]/curtain:flex\"\n                >\n                  {item.description}\n                </div>\n              </div>\n            ))}\n            <div\n              data-curtain-note\n              className=\"mt-6 flex flex-wrap justify-center gap-x-5 gap-y-3 font-mono text-[9px] uppercase\"\n            >\n              {footerLinks.map((link) => (\n                <a\n                  key={`${link.label}-${link.href}`}\n                  href={link.href}\n                  onClick={onClose}\n                  className=\"rounded-sm hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-current\"\n                >\n                  {link.label}\n                </a>\n              ))}\n            </div>\n          </nav>\n        </div>\n        <div\n          data-curtain-note\n          className=\"pointer-events-none absolute inset-x-0 bottom-0 flex min-h-16 items-end justify-between gap-4 px-5 pb-5 pt-5 font-mono text-[9px] uppercase leading-relaxed @[760px]/curtain:px-7\"\n          style={{\n            background: `linear-gradient(transparent, ${background} 45%)`,\n          }}\n        >\n          <p className=\"max-w-52 whitespace-pre-line\">{footerText}</p>\n          <span className=\"shrink-0\">Dev Club ©</span>\n        </div>\n      </div>\n      <CurtainHeader\n        brand={brand}\n        brandHref={brandHref}\n        logo={logo}\n        quickLinks={[]}\n        background={headerBackground}\n        foreground={headerForeground}\n        glitchDuration={glitchDuration}\n        onNavigate={onClose}\n        button={\n          <button\n            ref={closeRef}\n            type=\"button\"\n            aria-label=\"Close menu\"\n            onClick={onClose}\n            className=\"flex min-h-10 min-w-10 cursor-pointer items-center justify-center rounded-md hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current\"\n          >\n            <MenuMark open={open} />\n          </button>\n        }\n      />\n    </Dialog.Content>\n  );\n}\n\nexport function CurtainNavbar({\n  brand = \"devclub\",\n  brandHref = \"/\",\n  logo,\n  items = CURTAIN_NAVBAR_ITEMS,\n  quickLinks = DEFAULT_QUICK_LINKS,\n  footerLinks = DEFAULT_FOOTER_LINKS,\n  leftNote = \"A community of builders.\\nOpen source. Open minds.\",\n  rightNote = \"Made by Dev Club.\\nBuilt for the web.\",\n  footerText = \"Thoughtful interfaces.\\nMotion with purpose.\",\n  background = \"#67df32\",\n  foreground = \"#161616\",\n  headerBackground = \"#222222b3\",\n  headerForeground = \"#ffffff\",\n  duration = 0.85,\n  stagger = 0.045,\n  hoverDuration = 0.35,\n  closeSpeed = 1.2,\n  glitchDuration = 0.55,\n  hoverScroll = true,\n  scrollDuration = 0.8,\n  contained = false,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n}: CurtainNavbarProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const open = controlledOpen ?? internalOpen;\n  const [present, setPresent] = useState(open);\n  const [host, setHost] = useState<HTMLDivElement | null>(null);\n  const finishExit = useCallback(() => setPresent(false), []);\n  if (open && !present) setPresent(true);\n  const changeOpen = (next: boolean) => {\n    if (controlledOpen === undefined) setInternalOpen(next);\n    onOpenChange?.(next);\n  };\n  const settings: CurtainSettings = {\n    brand,\n    brandHref,\n    logo,\n    items,\n    quickLinks,\n    footerLinks,\n    leftNote,\n    rightNote,\n    footerText,\n    background,\n    foreground,\n    headerBackground,\n    headerForeground,\n    duration: clamp(duration, 0.3, 1.6, 0.85),\n    stagger: clamp(stagger, 0, 0.12, 0.045),\n    hoverDuration: clamp(hoverDuration, 0.15, 0.8, 0.35),\n    closeSpeed: clamp(closeSpeed, 0.7, 2, 1.2),\n    glitchDuration: clamp(glitchDuration, 0.15, 1.2, 0.55),\n    hoverScroll,\n    scrollDuration: clamp(scrollDuration, 0.2, 1.5, 0.8),\n    contained,\n  };\n  return (\n    <div\n      ref={setHost}\n      data-slot=\"curtain-navbar\"\n      className={cn(\n        \"pointer-events-none inset-0 z-30\",\n        contained ? \"absolute\" : \"fixed\",\n        className,\n      )}\n    >\n      <Dialog.Root open={present} onOpenChange={changeOpen}>\n        <div aria-hidden={present} inert={present}>\n          <CurtainHeader\n            brand={brand}\n            brandHref={brandHref}\n            logo={logo}\n            quickLinks={quickLinks}\n            background={headerBackground}\n            foreground={headerForeground}\n            glitchDuration={settings.glitchDuration}\n            button={\n              <Dialog.Trigger asChild>\n                <button\n                  type=\"button\"\n                  aria-label=\"Open menu\"\n                  className=\"flex min-h-10 min-w-10 cursor-pointer items-center justify-center rounded-md hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current\"\n                >\n                  <MenuMark open={false} />\n                </button>\n              </Dialog.Trigger>\n            }\n          />\n        </div>\n        <Dialog.Portal container={contained ? host : undefined}>\n          <CurtainPanel\n            open={open}\n            onClose={() => changeOpen(false)}\n            onExit={finishExit}\n            settings={settings}\n          />\n        </Dialog.Portal>\n      </Dialog.Root>\n    </div>\n  );\n}\n\nexport default CurtainNavbar;\n",
      "type": "registry:ui",
      "target": "@ui/curtain-navbar.tsx"
    }
  ]
}
