{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"navbar","type":"registry:ui","title":"NavBar","description":"Responsive navigation bar with built-in language selector, mobile menu, CTA button, and GitHub link.","dependencies":["clsx","tailwind-merge"],"registryDependencies":["https://elements.babelize.co/r/locale-types.json","https://elements.babelize.co/r/use-controllable-state.json"],"files":[{"path":"ui/navbar.tsx","type":"registry:ui","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\nimport type { Locale } from \"./types\";\n\nexport type { Locale };\n\nexport interface NavLink {\n  /** Visible link text */\n  label: string;\n  /** Destination passed to `linkComponent` as `href` */\n  href: string;\n}\n\nexport interface NavBarProps extends Omit<\n  React.ComponentPropsWithoutRef<\"nav\">,\n  \"defaultValue\" | \"onChange\"\n> {\n  /** Brand mark rendered on the left */\n  logo?: React.ReactNode;\n  /** Navigation links */\n  links?: NavLink[];\n  /** Locales offered by the built-in language menu */\n  locales?: Locale[];\n  /** Selected locale code. Provide this to control the component. */\n  value?: string;\n  /** Initial selected locale code when uncontrolled (default: \"en\") */\n  defaultValue?: string;\n  /** Callback when the locale changes */\n  onValueChange?: (code: string) => void;\n  /** Primary call-to-action button */\n  cta?: { label: string; href: string };\n  /** Show a GitHub icon link */\n  showGitHub?: boolean;\n  /** Target of the GitHub icon link */\n  githubUrl?: string;\n  /** Pin the bar to the top of the viewport (default: true) */\n  sticky?: boolean;\n  /** Show flag emojis in the language menu (default: true) */\n  showFlags?: boolean;\n  /**\n   * Component used to render internal links. Defaults to `\"a\"` so the\n   * component works in any React app; pass your router's link (e.g.\n   * `next/link`) to get client-side navigation.\n   */\n  linkComponent?: React.ElementType;\n}\n\nconst LOCALE_MAP: Record<string, { label: string; flag: string }> = {\n  en: { label: \"English\", flag: \"\\u{1F1FA}\\u{1F1F8}\" },\n  fr: { label: \"Fran\\u00e7ais\", flag: \"\\u{1F1EB}\\u{1F1F7}\" },\n  es: { label: \"Espa\\u00f1ol\", flag: \"\\u{1F1EA}\\u{1F1F8}\" },\n  de: { label: \"Deutsch\", flag: \"\\u{1F1E9}\\u{1F1EA}\" },\n  ja: { label: \"\\u65e5\\u672c\\u8a9e\", flag: \"\\u{1F1EF}\\u{1F1F5}\" },\n  ko: { label: \"\\ud55c\\uad6d\\uc5b4\", flag: \"\\u{1F1F0}\\u{1F1F7}\" },\n  zh: { label: \"\\u4e2d\\u6587\", flag: \"\\u{1F1E8}\\u{1F1F3}\" },\n  ar: { label: \"\\u0627\\u0644\\u0639\\u0631\\u0628\\u064a\\u0629\", flag: \"\\u{1F1F8}\\u{1F1E6}\" },\n  pt: { label: \"Portugu\\u00eas\", flag: \"\\u{1F1E7}\\u{1F1F7}\" },\n  it: { label: \"Italiano\", flag: \"\\u{1F1EE}\\u{1F1F9}\" },\n  nl: { label: \"Nederlands\", flag: \"\\u{1F1F3}\\u{1F1F1}\" },\n  ru: { label: \"\\u0420\\u0443\\u0441\\u0441\\u043a\\u0438\\u0439\", flag: \"\\u{1F1F7}\\u{1F1FA}\" },\n  pl: { label: \"Polski\", flag: \"\\u{1F1F5}\\u{1F1F1}\" },\n  tr: { label: \"T\\u00fcrk\\u00e7e\", flag: \"\\u{1F1F9}\\u{1F1F7}\" },\n  vi: { label: \"Ti\\u1ebfng Vi\\u1ec7t\", flag: \"\\u{1F1FB}\\u{1F1F3}\" },\n  th: { label: \"\\u0e44\\u0e17\\u0e22\", flag: \"\\u{1F1F9}\\u{1F1ED}\" },\n  id: { label: \"Bahasa Indonesia\", flag: \"\\u{1F1EE}\\u{1F1E9}\" },\n  hi: { label: \"\\u0939\\u093f\\u0928\\u094d\\u0926\\u0940\", flag: \"\\u{1F1EE}\\u{1F1F3}\" },\n  sv: { label: \"Svenska\", flag: \"\\u{1F1F8}\\u{1F1EA}\" },\n  da: { label: \"Dansk\", flag: \"\\u{1F1E9}\\u{1F1F0}\" },\n  fi: { label: \"Suomi\", flag: \"\\u{1F1EB}\\u{1F1EE}\" },\n  no: { label: \"Norsk\", flag: \"\\u{1F1F3}\\u{1F1F4}\" },\n  el: { label: \"\\u0395\\u03bb\\u03bb\\u03b7\\u03bd\\u03b9\\u03ba\\u03ac\", flag: \"\\u{1F1EC}\\u{1F1F7}\" },\n  he: { label: \"\\u05e2\\u05d1\\u05e8\\u05d9\\u05ea\", flag: \"\\u{1F1EE}\\u{1F1F1}\" },\n  ro: { label: \"Rom\\u00e2n\\u0103\", flag: \"\\u{1F1F7}\\u{1F1F4}\" },\n  hu: { label: \"Magyar\", flag: \"\\u{1F1ED}\\u{1F1FA}\" },\n  cs: { label: \"\\u010ce\\u0161tina\", flag: \"\\u{1F1E8}\\u{1F1FF}\" },\n  uk: {\n    label: \"\\u0423\\u043a\\u0440\\u0430\\u0457\\u043d\\u0441\\u044c\\u043a\\u0430\",\n    flag: \"\\u{1F1FA}\\u{1F1E6}\",\n  },\n  bg: {\n    label: \"\\u0411\\u044a\\u043b\\u0433\\u0430\\u0440\\u0441\\u043a\\u0438\",\n    flag: \"\\u{1F1E7}\\u{1F1EC}\",\n  },\n  hr: { label: \"Hrvatski\", flag: \"\\u{1F1ED}\\u{1F1F7}\" },\n  sk: { label: \"Sloven\\u010dina\", flag: \"\\u{1F1F8}\\u{1F1F0}\" },\n  sl: { label: \"Sloven\\u0161\\u010dina\", flag: \"\\u{1F1F8}\\u{1F1EE}\" },\n  lt: { label: \"Lietuvi\\u0173\", flag: \"\\u{1F1F1}\\u{1F1F9}\" },\n  lv: { label: \"Latvie\\u0161u\", flag: \"\\u{1F1F1}\\u{1F1FB}\" },\n  et: { label: \"Eesti\", flag: \"\\u{1F1EA}\\u{1F1EA}\" },\n  ca: { label: \"Catal\\u00e0\", flag: \"\\u{1F1E6}\\u{1F1F8}\" },\n  af: { label: \"Afrikaans\", flag: \"\\u{1F1E6}\\u{1F1FF}\" },\n  sw: { label: \"Kiswahili\", flag: \"\\u{1F1F9}\\u{1F1FF}\" },\n};\n\nfunction getLocaleInfo(code: string) {\n  return LOCALE_MAP[code] ?? { label: code.toUpperCase(), flag: \"\\u{1F310}\" };\n}\n\nexport const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar(\n  {\n    logo,\n    links = [],\n    locales = [],\n    value,\n    defaultValue,\n    onValueChange,\n    cta,\n    showGitHub = false,\n    githubUrl = \"https://github.com/babelize/babelize-elements\",\n    sticky = true,\n    className,\n    showFlags = true,\n    linkComponent: Link = \"a\",\n    ...rest\n  },\n  forwardedRef,\n) {\n  const [locale, setLocale] = useControllableState(\n    value,\n    defaultValue ?? locales[0]?.code ?? \"en\",\n    onValueChange,\n  );\n  const [mobileOpen, setMobileOpen] = React.useState(false);\n  // Which bar opened the language menu, rather than a single boolean. The desktop\n  // and mobile bars are both always in the DOM — only CSS hides one — so a shared\n  // boolean mounted two listboxes into the accessibility tree at once.\n  const [langOpen, setLangOpen] = React.useState<\"desktop\" | \"mobile\" | null>(null);\n  const langRef = React.useRef<HTMLDivElement>(null);\n  const mobileLangRef = React.useRef<HTMLDivElement>(null);\n  const menuId = React.useId();\n\n  const currentLocaleInfo = getLocaleInfo(locale);\n\n  React.useEffect(() => {\n    function handleClickOutside(e: MouseEvent) {\n      const target = e.target as Node;\n      // Both containers have to be checked. Guarding only the desktop one made a\n      // mousedown inside the mobile dropdown count as \"outside\", which closed the\n      // menu before the option's click event could land — so picking a language\n      // on mobile did nothing at all.\n      if (langRef.current?.contains(target) || mobileLangRef.current?.contains(target)) return;\n      setLangOpen(null);\n    }\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, []);\n\n  React.useEffect(() => {\n    function handleKeyDown(e: KeyboardEvent) {\n      if (e.key !== \"Escape\") return;\n      setLangOpen(null);\n      setMobileOpen(false);\n    }\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, []);\n\n  React.useEffect(() => {\n    if (!mobileOpen) return;\n    // Restore whatever the host page had set rather than assuming it was empty.\n    const previous = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = previous;\n    };\n  }, [mobileOpen]);\n\n  const langButton = (\n    <button\n      type=\"button\"\n      onClick={() => setLangOpen(langOpen === \"desktop\" ? null : \"desktop\")}\n      className=\"flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium text-zinc-500 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50\"\n      aria-expanded={langOpen === \"desktop\"}\n      aria-haspopup=\"listbox\"\n      aria-label={`Current language: ${currentLocaleInfo.label}`}\n    >\n      {showFlags && <span className=\"text-sm leading-none\">{currentLocaleInfo.flag}</span>}\n      <span className=\"hidden sm:inline\">{currentLocaleInfo.label}</span>\n      <svg\n        className={cn(\n          \"size-3 text-zinc-500 transition-transform dark:text-zinc-400\",\n          langOpen === \"desktop\" && \"rotate-180\",\n        )}\n        fill=\"none\"\n        viewBox=\"0 0 24 24\"\n        stroke=\"currentColor\"\n        strokeWidth={2}\n      >\n        <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n      </svg>\n    </button>\n  );\n\n  const langDropdown = (\n    <div\n      className=\"absolute right-0 top-full z-50 mt-1.5 min-w-[170px] rounded-xl border border-zinc-200 bg-white py-1.5 shadow-xl dark:border-zinc-800 dark:bg-zinc-950\"\n      role=\"listbox\"\n      aria-label=\"Select language\"\n    >\n      {locales.map((item) => {\n        const info = getLocaleInfo(item.code);\n        const isActive = item.code === locale;\n        return (\n          <button\n            key={item.code}\n            type=\"button\"\n            role=\"option\"\n            aria-selected={isActive}\n            onClick={() => {\n              setLocale(item.code);\n              setLangOpen(null);\n            }}\n            className={cn(\n              \"mx-1.5 flex w-[calc(100%-12px)] items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-colors\",\n              isActive\n                ? \"bg-emerald-500/10 text-emerald-600 font-medium dark:text-emerald-400\"\n                : \"text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100\",\n            )}\n          >\n            {showFlags && <span className=\"text-base leading-none\">{info.flag}</span>}\n            <span className=\"flex-1\">{item.label ?? info.label}</span>\n            {isActive && (\n              <svg\n                className=\"size-4 shrink-0 text-emerald-600 dark:text-emerald-400\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n                strokeWidth={2.5}\n              >\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n              </svg>\n            )}\n          </button>\n        );\n      })}\n    </div>\n  );\n\n  return (\n    <nav\n      ref={forwardedRef}\n      {...rest}\n      className={cn(\n        \"z-50 w-full border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900\",\n        sticky && \"sticky top-0\",\n        className,\n      )}\n    >\n      {/* Desktop */}\n      <div className=\"mx-auto hidden max-w-6xl items-center justify-between px-5 py-3 md:flex\">\n        {logo && <div className=\"flex-shrink-0\">{logo}</div>}\n\n        <div className=\"flex items-center gap-1\">\n          {links.map((link) => (\n            <Link\n              key={link.href}\n              href={link.href}\n              className=\"px-3 py-1.5 text-sm transition-colors rounded-md no-underline text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50\"\n            >\n              {link.label}\n            </Link>\n          ))}\n        </div>\n\n        <div className=\"flex items-center gap-1\">\n          {locales.length > 0 && (\n            <div ref={langRef} className=\"relative\">\n              {langButton}\n              {langOpen === \"desktop\" && langDropdown}\n            </div>\n          )}\n\n          {showGitHub && (\n            <a\n              href={githubUrl}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex size-8 items-center justify-center rounded-md transition-colors no-underline text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50\"\n              aria-label=\"GitHub\"\n            >\n              <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n                <path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" />\n              </svg>\n            </a>\n          )}\n\n          {cta && (\n            <Link\n              href={cta.href}\n              className=\"rounded-md px-3.5 py-1.5 text-sm font-medium transition-colors no-underline bg-emerald-500 text-white hover:opacity-90\"\n            >\n              {cta.label}\n            </Link>\n          )}\n        </div>\n      </div>\n\n      {/* Mobile */}\n      <div className=\"flex items-center justify-between px-4 py-3 md:hidden\">\n        {logo && <div className=\"flex-shrink-0\">{logo}</div>}\n\n        <div className=\"flex items-center gap-1\">\n          {locales.length > 0 && (\n            <div ref={mobileLangRef} className=\"relative\">\n              <button\n                type=\"button\"\n                onClick={() => setLangOpen(langOpen === \"mobile\" ? null : \"mobile\")}\n                className=\"inline-flex size-8 items-center justify-center rounded-md transition-colors text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50\"\n                aria-expanded={langOpen === \"mobile\"}\n                aria-haspopup=\"listbox\"\n                aria-label={`Current language: ${currentLocaleInfo.label}`}\n              >\n                {showFlags && (\n                  <span className=\"text-base leading-none\">{currentLocaleInfo.flag}</span>\n                )}\n              </button>\n              {langOpen === \"mobile\" && langDropdown}\n            </div>\n          )}\n\n          <button\n            type=\"button\"\n            className=\"inline-flex size-8 items-center justify-center rounded-md transition-colors text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50\"\n            onClick={() => setMobileOpen(!mobileOpen)}\n            aria-label=\"Toggle menu\"\n            aria-expanded={mobileOpen}\n            aria-controls={mobileOpen ? menuId : undefined}\n          >\n            {mobileOpen ? (\n              <svg\n                className=\"size-4\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n                strokeWidth={2}\n              >\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\" />\n              </svg>\n            ) : (\n              <svg\n                className=\"size-4\"\n                fill=\"none\"\n                viewBox=\"0 0 24 24\"\n                stroke=\"currentColor\"\n                strokeWidth={2}\n              >\n                <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M4 6h16M4 12h16M4 18h16\" />\n              </svg>\n            )}\n          </button>\n        </div>\n      </div>\n\n      {/* Mobile menu panel */}\n      {mobileOpen && (\n        <div\n          id={menuId}\n          className=\"border-t border-zinc-200 bg-white px-4 py-3 md:hidden dark:border-zinc-800 dark:bg-zinc-900\"\n        >\n          {links.map((link) => (\n            <Link\n              key={link.href}\n              href={link.href}\n              onClick={() => setMobileOpen(false)}\n              className=\"block py-2.5 text-sm transition-colors no-underline text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50\"\n            >\n              {link.label}\n            </Link>\n          ))}\n          {cta && (\n            <Link\n              href={cta.href}\n              onClick={() => setMobileOpen(false)}\n              className=\"mt-2 block rounded-md px-3.5 py-2 text-center text-sm font-medium transition-colors no-underline bg-emerald-500 text-white hover:opacity-90\"\n            >\n              {cta.label}\n            </Link>\n          )}\n        </div>\n      )}\n    </nav>\n  );\n});\n"}]}