Navigation Bar

Top-level application navigation bar.

Installation

$npx @309-thingspire/ui@latest add navigation-bar

Usage

import { NavigationBar } from "@/components/navigation-bar/navigation-bar"
<NavigationBar />

Examples

Live preview rendered from NavigationBar.preview.tsx. Switch to the Code tab to view the underlying component source.

Loading preview…
import React, { useEffect, useRef, useState } from 'react';
import type { CSSProperties, ReactNode } from 'react';

import { border, colors, radius, shadows, spacing, typography } from '../../style-tokens';
import { IconArrowDownSLine, IconCheckLine, IconGlobalLine, IconMenuLine } from '../icons';
import { AvatarFallbackSvg, ProAccessSvg, ThingspireWordmark } from './NavigationBar.assets';

import type {
  NavigationBarInteractionState,
  NavigationBarLanguageItem,
  NavigationBarLinkItem,
  NavigationBarProps,
  NavigationBarType,
} from './NavigationBar.types';

type TypographyToken = {
  fontFamily: string;
  fontSize: number;
  fontWeight: number;
  lineHeight: number;
  letterSpacing: number;
};

const palette = colors.primitive.palette;
const textBase = colors.semantic.theme.text.base;
const textAccent = colors.semantic.theme.text.accent;

const NAV_DEFAULT_WIDTH = spacing.scale['1440'];
const NAV_HEIGHT = spacing.scale['64'];
const NAV_HEIGHT_DOUBLE = spacing.scale['112'];
// Wordmark frame: w 100 (32 × 3 + 4) × h 32 per Figma carbonscope-Library v1.0
// (node 1108:19278). Native vector is 85.501 × 21.204 — frame adds ~7px
// horizontal and ~5px vertical padding so the wordmark sits centered.
const LOGO_FRAME_WIDTH = spacing.scale['96'] + spacing.scale['4'];
const LOGO_FRAME_HEIGHT = spacing.scale['32'];
const LOGO_VECTOR_WIDTH = 85.501;
const LOGO_VECTOR_HEIGHT = 21.204;
const ICON_SIZE = spacing.scale['16'];
const SEARCH_MAX_WIDTH = spacing.primitive['360'];

const DEFAULT_MAIN_LINKS: NavigationBarLinkItem[] = [
  { id: 'library', label: 'Library' },
  { id: 'studio', label: 'Studio' },
  { id: 'pronunciation-dictionary', label: 'Pronunciation Dictionary' },
  { id: 'voice', label: 'Voice' },
];

const TYPE06_DEFAULT_LINKS: NavigationBarLinkItem[] = [
  { id: 'explore', label: 'Explore' },
  { id: 'pro-access', label: 'Pro Access', accent: true },
];

const TYPE07_DEFAULT_LINKS: NavigationBarLinkItem[] = [
  { id: 'library', label: 'Library' },
  { id: 'studio', label: 'Studio' },
  { id: 'pronunciation-dictionary', label: 'Pronunciation Dictionary' },
  { id: 'voice-cloning', label: 'Voice Cloning', hasChevron: true },
];

const TYPE02_DEFAULT_LANGUAGE_ITEMS: NavigationBarLanguageItem[] = [
  { id: 'ko', label: '한국어' },
  { id: 'en', label: 'English' },
  { id: 'ja', label: '日本語' },
  { id: 'zh', label: '中文' },
];

const TYPE08_DEFAULT_BOTTOM_LINKS: NavigationBarLinkItem[] = [
  { id: 'library', label: 'Library' },
  { id: 'studio', label: 'Studio', badgeText: '12' },
  { id: 'pronunciation-dictionary', label: 'Pronunciation Dictionary' },
  { id: 'voice-cloning', label: 'Voice Cloning', badgeText: '08' },
];

function toTypographyStyle(token: TypographyToken): CSSProperties {
  return {
    fontFamily: token.fontFamily,
    fontSize: token.fontSize,
    fontWeight: token.fontWeight,
    lineHeight: `${token.lineHeight}px`,
    letterSpacing: `${token.letterSpacing}px`,
  };
}

function withFocusRing(baseShadow: string, interactionState: NavigationBarInteractionState, disabled: boolean): string {
  if (interactionState === 'focus' && !disabled) {
    return `${baseShadow}, ${shadows.focusRing.light.css}`;
  }

  return baseShadow;
}

function getMainLinks(type: NavigationBarType, links: NavigationBarLinkItem[] | undefined): NavigationBarLinkItem[] {
  if (links && links.length > 0) {
    return links;
  }

  if (type === '06') {
    return TYPE06_DEFAULT_LINKS;
  }

  if (type === '07') {
    return TYPE07_DEFAULT_LINKS;
  }

  return DEFAULT_MAIN_LINKS;
}

function getBottomLinks(bottomLinks: NavigationBarLinkItem[] | undefined): NavigationBarLinkItem[] {
  if (bottomLinks && bottomLinks.length > 0) {
    return bottomLinks;
  }

  return TYPE08_DEFAULT_BOTTOM_LINKS;
}

function IconImage({ children, size = ICON_SIZE }: { children: ReactNode; size?: number }) {
  return (
    <span
      aria-hidden="true"
      style={{
        width: size,
        height: size,
        display: 'block',
        userSelect: 'none',
        pointerEvents: 'none',
      }}
    >
      {children}
    </span>
  );
}

function LogoMark() {
  return (
    <span
      aria-hidden="true"
      style={{
        width: LOGO_FRAME_WIDTH,
        height: LOGO_FRAME_HEIGHT,
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        flexShrink: 0,
      }}
    >
      <span
        role="img"
        aria-label="thingspire"
        style={{
          width: LOGO_VECTOR_WIDTH,
          height: LOGO_VECTOR_HEIGHT,
          display: 'block',
          userSelect: 'none',
          pointerEvents: 'none',
        }}
      >
        <ThingspireWordmark />
      </span>
    </span>
  );
}

function SearchField({
  placeholder,
  shortcutLabel,
  interactionState,
  disabled,
}: {
  placeholder: string;
  shortcutLabel: string;
  interactionState: NavigationBarInteractionState;
  disabled: boolean;
}) {
  const fieldBorderColor = disabled ? palette.gray['2'] : palette.gray['3'];
  const fieldBackground = disabled ? palette.gray['1'] : palette.base.white;
  const placeholderColor = disabled ? textBase.staticDarkQuaternary : textBase.staticDarkTertiary;
  const shortcutColor = disabled ? textBase.staticDarkQuaternary : textBase.staticDarkSecondary;

  return (
    <div
      style={{
        width: '100%',
        maxWidth: SEARCH_MAX_WIDTH,
        minWidth: spacing.scale['144'],
        boxShadow: interactionState === 'focus' && !disabled ? shadows.focusRing.light.css : 'none',
      }}
    >
      <div
        style={{
          width: '100%',
          display: 'flex',
          alignItems: 'center',
          gap: spacing.scale['4'],
          borderStyle: 'solid',
          borderWidth: border.width['1'],
          borderColor: fieldBorderColor,
          borderRadius: radius.scale.lg,
          backgroundColor: fieldBackground,
          paddingInline: spacing.scale['8'],
          paddingBlock: spacing.scale['6'],
          boxSizing: 'border-box',
        }}
      >
        <span
          style={{
            flex: '1 0 0',
            minWidth: spacing.scale['0'],
            color: placeholderColor,
            ...toTypographyStyle(typography.scale.captionL.regular),
          }}
        >
          {placeholder}
        </span>

        <span
          aria-hidden="true"
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            borderRadius: radius.scale.sm,
            backgroundColor: disabled ? palette.gray['2a'] : palette.gray['2'],
            paddingInline: spacing.scale['8'],
            paddingBlock: spacing.scale['2'],
            color: shortcutColor,
            ...toTypographyStyle(typography.scale.captionL.medium),
            whiteSpace: 'nowrap',
          }}
        >
          {shortcutLabel}
        </span>
      </div>
    </div>
  );
}

function NavigationMainItem({
  item,
  interactionState,
  componentDisabled,
  onClick,
}: {
  item: NavigationBarLinkItem;
  interactionState: NavigationBarInteractionState;
  componentDisabled: boolean;
  onClick?: (id: string) => void;
}) {
  const disabled = componentDisabled || item.disabled;
  const textColor = disabled ? textBase.staticDarkQuaternary : item.accent ? textAccent.blueAccent : textBase.staticDarkSecondary;
  const hasProIcon = item.id === 'pro-access';
  const hasLeadingIcon = Boolean(item.icon) || hasProIcon;
  const hasTrailingChevron = item.hasChevron;
  const isInteractive = Boolean(onClick) && !disabled;

  let iconNode: ReactNode = null;

  if (item.icon) {
    iconNode = item.icon;
  } else if (hasProIcon) {
    iconNode = <IconImage><ProAccessSvg /></IconImage>;
  }

  return (
    <button
      type="button"
      disabled={disabled}
      onClick={isInteractive ? () => onClick?.(item.id) : undefined}
      aria-disabled={disabled || undefined}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        gap: hasLeadingIcon || hasTrailingChevron ? spacing.scale['6'] : spacing.scale['0'],
        borderStyle: 'solid',
        borderWidth: border.width['0'],
        backgroundColor: palette.base.transparent,
        paddingInline: spacing.scale['0'],
        paddingBlock: spacing.scale['0'],
        margin: spacing.scale['0'],
        color: textColor,
        cursor: isInteractive ? 'pointer' : 'default',
        boxShadow: interactionState === 'focus' && !disabled ? shadows.focusRing.light.css : 'none',
      }}
    >
      {iconNode}

      <span
        style={{
          whiteSpace: 'nowrap',
          textAlign: 'center',
          ...toTypographyStyle(typography.scale.captionL.medium),
        }}
      >
        {item.label}
      </span>

      {hasTrailingChevron ? (
        <IconArrowDownSLine
          aria-hidden
          style={{ width: ICON_SIZE, height: ICON_SIZE, display: 'block' }}
        />
      ) : null}
    </button>
  );
}

function NavigationBottomTabItem({
  item,
  active,
  interactionState,
  componentDisabled,
  onClick,
}: {
  item: NavigationBarLinkItem;
  active: boolean;
  interactionState: NavigationBarInteractionState;
  componentDisabled: boolean;
  onClick?: (id: string) => void;
}) {
  const disabled = componentDisabled || item.disabled;
  const textColor = disabled ? textBase.staticDarkQuaternary : textBase.staticDarkSecondary;
  const badgeBackground = disabled ? palette.gray['2a'] : palette.gray['1a'];
  const badgeTextColor = disabled ? textBase.staticDarkQuaternary : active ? textBase.staticDark : textBase.staticDarkSecondary;
  const isInteractive = Boolean(onClick) && !disabled;

  return (
    <button
      type="button"
      disabled={disabled}
      onClick={isInteractive ? () => onClick?.(item.id) : undefined}
      aria-current={active ? 'page' : undefined}
      aria-disabled={disabled || undefined}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        gap: spacing.scale['8'],
        borderStyle: 'solid',
        borderWidth: border.width['0'],
        borderBottomWidth: active ? border.width['2'] : border.width['0'],
        borderBottomColor: active ? border.color.theme.select.primary : palette.base.transparent,
        backgroundColor: palette.base.transparent,
        paddingInline: spacing.scale['0'],
        paddingTop: spacing.scale['10'],
        paddingBottom: spacing.scale['14'],
        margin: spacing.scale['0'],
        cursor: isInteractive ? 'pointer' : 'default',
        boxShadow: interactionState === 'focus' && !disabled ? shadows.focusRing.light.css : 'none',
      }}
    >
      <span
        style={{
          whiteSpace: 'nowrap',
          textAlign: 'center',
          color: textColor,
          ...toTypographyStyle(typography.scale.captionL.medium),
        }}
      >
        {item.label}
      </span>

      {item.badgeText ? (
        <span
          aria-hidden="true"
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            borderRadius: radius.scale.md,
            backgroundColor: badgeBackground,
            paddingInline: spacing.scale['8'],
            paddingBlock: spacing.scale['2'],
            color: badgeTextColor,
            ...toTypographyStyle(typography.scale.captionL.medium),
            whiteSpace: 'nowrap',
          }}
        >
          {item.badgeText}
        </span>
      ) : null}
    </button>
  );
}

function BaseCenterLinks({
  links,
  interactionState,
  componentDisabled,
  onLinkClick,
  justify,
}: {
  links: NavigationBarLinkItem[];
  interactionState: NavigationBarInteractionState;
  componentDisabled: boolean;
  onLinkClick?: (id: string) => void;
  justify: 'flex-start' | 'center';
}) {
  return (
    <div
      style={{
        display: 'flex',
        flex: '1 0 0',
        alignItems: 'center',
        justifyContent: justify,
        gap: spacing.scale['24'],
        minWidth: spacing.scale['0'],
      }}
    >
      {links.map((item) => (
        <NavigationMainItem
          key={item.id}
          item={item}
          interactionState={interactionState}
          componentDisabled={componentDisabled}
          onClick={onLinkClick}
        />
      ))}
    </div>
  );
}

interface NavigationBarType02Props {
  id?: string;
  className?: string;
  style?: CSSProperties;
  commonRootStyle: CSSProperties;
  interactionState: NavigationBarInteractionState;
  componentDisabled: boolean;
  showMenuButton: boolean;
  languageLabel?: string;
  languageItems?: NavigationBarLanguageItem[];
  selectedLanguageId?: string;
  defaultSelectedLanguageId?: string;
  languageMenuOpen?: boolean;
  defaultLanguageMenuOpen: boolean;
  searchPlaceholder: string;
  searchShortcutLabel: string;
  onMenuClick?: () => void;
  onLanguageClick?: () => void;
  onLanguageMenuOpenChange?: (open: boolean) => void;
  onLanguageChange?: (id: string) => void;
  rest: Record<string, unknown>;
}

function NavigationBarType02({
  id,
  className,
  style,
  commonRootStyle,
  interactionState,
  componentDisabled,
  showMenuButton,
  languageLabel,
  languageItems,
  selectedLanguageId,
  defaultSelectedLanguageId,
  languageMenuOpen,
  defaultLanguageMenuOpen,
  searchPlaceholder,
  searchShortcutLabel,
  onMenuClick,
  onLanguageClick,
  onLanguageMenuOpenChange,
  onLanguageChange,
  rest,
}: NavigationBarType02Props) {
  const resolvedItems = languageItems && languageItems.length > 0 ? languageItems : TYPE02_DEFAULT_LANGUAGE_ITEMS;

  const isSelectionControlled = typeof selectedLanguageId === 'string';
  const [internalSelectedId, setInternalSelectedId] = useState<string>(
    () => defaultSelectedLanguageId ?? resolvedItems[0]?.id ?? '',
  );
  const activeSelectedId = isSelectionControlled ? selectedLanguageId : internalSelectedId;

  const selectedItem =
    resolvedItems.find((item) => item.id === activeSelectedId) ?? resolvedItems[0];
  const triggerLabel = languageLabel ?? selectedItem?.label ?? '';

  const isOpenControlled = typeof languageMenuOpen === 'boolean';
  const [internalOpen, setInternalOpen] = useState<boolean>(defaultLanguageMenuOpen);
  const isOpen = isOpenControlled ? languageMenuOpen : internalOpen;

  const wrapperRef = useRef<HTMLDivElement | null>(null);

  const updateOpen = (next: boolean) => {
    if (!isOpenControlled) {
      setInternalOpen(next);
    }
    onLanguageMenuOpenChange?.(next);
  };

  useEffect(() => {
    if (!isOpen) {
      return;
    }

    const handlePointerDown = (event: MouseEvent) => {
      if (!wrapperRef.current) {
        return;
      }
      if (!wrapperRef.current.contains(event.target as Node)) {
        updateOpen(false);
      }
    };

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        updateOpen(false);
      }
    };

    document.addEventListener('mousedown', handlePointerDown);
    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('mousedown', handlePointerDown);
      document.removeEventListener('keydown', handleKeyDown);
    };
    // updateOpen is stable for this lifecycle; intentionally skip the deps lint
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isOpen, isOpenControlled]);

  const handleTriggerClick = () => {
    if (componentDisabled) {
      return;
    }
    updateOpen(!isOpen);
    onLanguageClick?.();
  };

  const handleItemClick = (id: string) => {
    if (!isSelectionControlled) {
      setInternalSelectedId(id);
    }
    onLanguageChange?.(id);
    updateOpen(false);
  };

  const ghostBorderColor = palette.base.transparent;
  const languageTextColor = componentDisabled ? textBase.staticDarkQuaternary : textBase.staticDarkSecondary;
  const iconButtonColor = componentDisabled ? textBase.staticDarkQuaternary : textBase.staticDarkSecondary;
  const placeholderText = searchPlaceholder === 'Search...' ? 'Placeholder' : searchPlaceholder;
  const shortcutText = searchShortcutLabel === '/' ? '' : searchShortcutLabel;
  const triggerBackground = isOpen && !componentDisabled ? palette.gray['1a'] : palette.base.transparent;

  return (
    <header
      id={id}
      className={className}
      aria-disabled={componentDisabled || undefined}
      style={{
        ...commonRootStyle,
        minHeight: NAV_HEIGHT,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'space-between',
        paddingLeft: spacing.scale['16'],
        paddingRight: spacing.scale['32'],
        paddingTop: spacing.scale['16'],
        paddingBottom: spacing.scale['16'],
        borderBottomStyle: 'solid',
        borderBottomWidth: border.width['1'],
        borderBottomColor: componentDisabled ? palette.gray['2'] : palette.gray['3'],
        backgroundColor: palette.base.white,
        boxSizing: 'border-box',
        ...style,
      }}
      {...rest}
    >
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: spacing.scale['0'],
        }}
      >
        {showMenuButton ? (
          <button
            type="button"
            aria-label="Menu"
            disabled={componentDisabled}
            onClick={!componentDisabled ? onMenuClick : undefined}
            style={{
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center',
              padding: spacing.scale['8'],
              borderStyle: 'solid',
              borderWidth: border.width['0'],
              borderColor: ghostBorderColor,
              borderRadius: radius.scale.lg,
              backgroundColor: palette.base.transparent,
              color: iconButtonColor,
              cursor: componentDisabled ? 'default' : 'pointer',
              appearance: 'none',
              outline: 'none',
              marginRight: spacing.scale['4'],
            }}
          >
            <IconMenuLine
              aria-hidden
              style={{ width: ICON_SIZE, height: ICON_SIZE, display: 'block' }}
            />
          </button>
        ) : null}

        <LogoMark />
      </div>

      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'flex-end',
          gap: spacing.scale['16'],
        }}
      >
        <div style={{ width: spacing.primitive['256'] + spacing.scale['24'] }}>
          <SearchField
            placeholder={placeholderText}
            shortcutLabel={shortcutText}
            interactionState={interactionState}
            disabled={componentDisabled}
          />
        </div>

        <div ref={wrapperRef} style={{ position: 'relative' }}>
          <button
            type="button"
            aria-haspopup="listbox"
            aria-expanded={isOpen}
            aria-disabled={componentDisabled || undefined}
            disabled={componentDisabled}
            onClick={handleTriggerClick}
            style={{
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center',
              gap: spacing.scale['2'],
              paddingInline: spacing.scale['10'],
              paddingBlock: spacing.scale['6'],
              borderStyle: 'solid',
              borderWidth: border.width['0'],
              borderColor: ghostBorderColor,
              borderRadius: radius.scale.lg,
              backgroundColor: triggerBackground,
              color: languageTextColor,
              cursor: componentDisabled ? 'default' : 'pointer',
              appearance: 'none',
              outline: 'none',
              boxShadow: interactionState === 'focus' && !componentDisabled ? shadows.focusRing.light.css : 'none',
              transition: 'background-color 120ms ease',
            }}
          >
            <IconGlobalLine
              aria-hidden
              style={{ width: ICON_SIZE, height: ICON_SIZE, display: 'block' }}
            />
            <span
              style={{
                paddingInline: spacing.scale['4'],
                whiteSpace: 'nowrap',
                ...toTypographyStyle(typography.scale.captionL.medium),
              }}
            >
              {triggerLabel}
            </span>
            <IconArrowDownSLine
              aria-hidden
              style={{
                width: ICON_SIZE,
                height: ICON_SIZE,
                display: 'block',
                transform: isOpen ? 'rotate(180deg)' : 'none',
                transition: 'transform 160ms ease',
              }}
            />
          </button>

          {isOpen ? (
            <ul
              role="listbox"
              aria-label="Language"
              style={{
                position: 'absolute',
                top: `calc(100% + ${spacing.scale['6']}px)`,
                right: 0,
                margin: 0,
                padding: spacing.scale['4'],
                listStyle: 'none',
                minWidth: spacing.scale['144'],
                backgroundColor: palette.base.white,
                borderStyle: 'solid',
                borderWidth: border.width['1'],
                borderColor: palette.gray['3'],
                borderRadius: radius.scale.lg,
                boxShadow: shadows.elevation.md.css,
                zIndex: 50,
              }}
            >
              {resolvedItems.map((item) => {
                const isActive = item.id === activeSelectedId;
                return (
                  <li key={item.id} role="presentation" style={{ margin: 0 }}>
                    <button
                      type="button"
                      role="option"
                      aria-selected={isActive}
                      onClick={() => handleItemClick(item.id)}
                      style={{
                        width: '100%',
                        display: 'inline-flex',
                        alignItems: 'center',
                        justifyContent: 'space-between',
                        gap: spacing.scale['8'],
                        paddingInline: spacing.scale['8'],
                        paddingBlock: spacing.scale['6'],
                        borderStyle: 'solid',
                        borderWidth: border.width['0'],
                        borderRadius: radius.scale.md,
                        backgroundColor: isActive ? palette.gray['1a'] : palette.base.transparent,
                        color: textBase.staticDark,
                        cursor: 'pointer',
                        appearance: 'none',
                        outline: 'none',
                        textAlign: 'left',
                        ...toTypographyStyle(typography.scale.captionL.medium),
                      }}
                      onMouseEnter={(event) => {
                        if (!isActive) {
                          event.currentTarget.style.backgroundColor = palette.gray['1a'];
                        }
                      }}
                      onMouseLeave={(event) => {
                        if (!isActive) {
                          event.currentTarget.style.backgroundColor = palette.base.transparent;
                        }
                      }}
                    >
                      <span style={{ display: 'inline-flex', flexDirection: 'column', alignItems: 'flex-start', gap: spacing.scale['2'] }}>
                        <span style={{ whiteSpace: 'nowrap' }}>{item.label}</span>
                        {item.caption ? (
                          <span
                            style={{
                              color: textBase.staticDarkSecondary,
                              ...toTypographyStyle(typography.scale.captionM.regular),
                            }}
                          >
                            {item.caption}
                          </span>
                        ) : null}
                      </span>
                      {isActive ? (
                        <IconCheckLine
                          aria-hidden
                          style={{ width: ICON_SIZE, height: ICON_SIZE, display: 'block', flexShrink: 0 }}
                        />
                      ) : null}
                    </button>
                  </li>
                );
              })}
            </ul>
          ) : null}
        </div>
      </div>
    </header>
  );
}

export function NavigationBar({
  id,
  className,
  style,
  type = '01',
  width = NAV_DEFAULT_WIDTH,
  interactionState = 'default',
  links,
  bottomLinks,
  activeBottomLinkId,
  searchPlaceholder = 'Search...',
  searchShortcutLabel = '/',
  ctaLabel = 'Try for free',
  helpLabel = 'Help',
  avatarSrc,
  showMenuButton = true,
  languageLabel,
  languageItems,
  selectedLanguageId,
  defaultSelectedLanguageId,
  languageMenuOpen,
  defaultLanguageMenuOpen = false,
  onMenuClick,
  onLanguageClick,
  onLanguageMenuOpenChange,
  onLanguageChange,
  onLinkClick,
  onCtaClick,
  onBottomLinkClick,
  ...props
}: NavigationBarProps) {
  const componentDisabled = interactionState === 'disabled';
  const resolvedMainLinks = getMainLinks(type, links);
  const resolvedBottomLinks = getBottomLinks(bottomLinks);
  const resolvedActiveBottomLinkId =
    activeBottomLinkId ?? resolvedBottomLinks[spacing.scale['1']]?.id ?? resolvedBottomLinks[spacing.scale['0']]?.id ?? '';

  const commonRootStyle: CSSProperties = {
    width,
    boxSizing: 'border-box',
  };

  if (type === '02') {
    return (
      <NavigationBarType02
        id={id}
        className={className}
        style={style}
        commonRootStyle={commonRootStyle}
        interactionState={interactionState}
        componentDisabled={componentDisabled}
        showMenuButton={showMenuButton}
        languageLabel={languageLabel}
        languageItems={languageItems}
        selectedLanguageId={selectedLanguageId}
        defaultSelectedLanguageId={defaultSelectedLanguageId}
        languageMenuOpen={languageMenuOpen}
        defaultLanguageMenuOpen={defaultLanguageMenuOpen}
        searchPlaceholder={searchPlaceholder}
        searchShortcutLabel={searchShortcutLabel}
        onMenuClick={onMenuClick}
        onLanguageClick={onLanguageClick}
        onLanguageMenuOpenChange={onLanguageMenuOpenChange}
        onLanguageChange={onLanguageChange}
        rest={props}
      />
    );
  }


  if (type === '07') {
    const ctaTextColor = componentDisabled ? textBase.staticDarkQuaternary : textBase.staticWhite;
    const ctaBackground = componentDisabled ? palette.gray['3'] : palette.gray['13'];

    return (
      <header
        id={id}
        className={className}
        aria-disabled={componentDisabled || undefined}
        style={{
          ...commonRootStyle,
          minHeight: NAV_HEIGHT,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          paddingInline: spacing.scale['120'],
          paddingBlock: spacing.scale['16'],
          ...style,
        }}
        {...props}
      >
        <div
          style={{
            width: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            gap: spacing.scale['32'],
            paddingInline: spacing.scale['16'],
            paddingBlock: spacing.scale['8'],
            borderStyle: 'solid',
            borderWidth: border.width['1'],
            borderColor: componentDisabled ? palette.gray['2'] : palette.gray['3'],
            borderRadius: radius.scale.xxl,
            backgroundColor: palette.base.white,
            boxShadow: shadows.elevation.lg.css,
            boxSizing: 'border-box',
          }}
        >
          <div
            style={{
              display: 'flex',
              flex: '1 0 0',
              alignItems: 'center',
            }}
          >
            <LogoMark />
          </div>

          <BaseCenterLinks
            links={resolvedMainLinks}
            interactionState={interactionState}
            componentDisabled={componentDisabled}
            onLinkClick={onLinkClick}
            justify="center"
          />

          <div
            style={{
              display: 'flex',
              flex: '1 0 0',
              alignItems: 'center',
              justifyContent: 'flex-end',
            }}
          >
            <button
              type="button"
              disabled={componentDisabled}
              onClick={!componentDisabled ? onCtaClick : undefined}
              aria-disabled={componentDisabled || undefined}
              style={{
                display: 'inline-flex',
                alignItems: 'center',
                justifyContent: 'center',
                borderStyle: 'solid',
                borderWidth: border.width['0'],
                borderRadius: radius.scale.lg,
                backgroundColor: ctaBackground,
                color: ctaTextColor,
                paddingInline: spacing.scale['10'],
                paddingBlock: spacing.scale['6'],
                boxShadow: withFocusRing(shadows.elevation.xs.css, interactionState, componentDisabled),
                cursor: componentDisabled ? 'default' : 'pointer',
              }}
            >
              <span
                style={{
                  display: 'inline-flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  paddingInline: spacing.scale['4'],
                  paddingBlock: spacing.scale['0'],
                  whiteSpace: 'nowrap',
                  ...toTypographyStyle(typography.scale.captionL.medium),
                }}
              >
                {ctaLabel}
              </span>
            </button>
          </div>
        </div>
      </header>
    );
  }

  if (type === '08') {
    const helpTextColor = componentDisabled ? textBase.staticDarkQuaternary : textBase.staticDarkSecondary;

    return (
      <header
        id={id}
        className={className}
        aria-disabled={componentDisabled || undefined}
        style={{
          ...commonRootStyle,
          minHeight: NAV_HEIGHT_DOUBLE,
          display: 'flex',
          flexDirection: 'column',
          backgroundColor: palette.base.white,
          ...style,
        }}
        {...props}
      >
        <div
          style={{
            width: '100%',
            minHeight: NAV_HEIGHT,
            display: 'flex',
            alignItems: 'center',
            gap: spacing.scale['32'],
            paddingLeft: spacing.scale['24'],
            paddingRight: spacing.scale['64'],
            paddingTop: spacing.scale['16'],
            paddingBottom: spacing.scale['16'],
            boxSizing: 'border-box',
          }}
        >
          <LogoMark />

          <div
            style={{
              display: 'flex',
              flex: '1 0 0',
              justifyContent: 'center',
              minWidth: spacing.scale['0'],
            }}
          >
            <SearchField
              placeholder={searchPlaceholder}
              shortcutLabel={searchShortcutLabel}
              interactionState={interactionState}
              disabled={componentDisabled}
            />
          </div>

          <span
            style={{
              width: spacing.scale['32'],
              height: spacing.scale['32'],
              borderRadius: radius.scale.full,
              overflow: 'hidden',
              flexShrink: 0,
              display: 'inline-flex',
            }}
          >
            {avatarSrc ? (
              <img
                src={avatarSrc}
                alt=""
                aria-hidden="true"
                style={{
                  width: '100%',
                  height: '100%',
                  objectFit: 'cover',
                  display: 'block',
                  userSelect: 'none',
                  pointerEvents: 'none',
                }}
              />
            ) : (
              <AvatarFallbackSvg />
            )}
          </span>
        </div>

        <div
          style={{
            width: '100%',
            minHeight: NAV_HEIGHT - spacing.scale['16'],
            display: 'flex',
            alignItems: 'center',
            gap: spacing.scale['32'],
            paddingLeft: spacing.scale['24'],
            paddingRight: spacing.scale['64'],
            borderBottomStyle: 'solid',
            borderBottomWidth: border.width['1'],
            borderBottomColor: palette.gray['2'],
            boxSizing: 'border-box',
          }}
        >
          <div
            style={{
              display: 'flex',
              flex: '1 0 0',
              alignItems: 'center',
              minWidth: spacing.scale['0'],
            }}
          >
            <div
              style={{
                display: 'inline-flex',
                alignItems: 'center',
                gap: spacing.scale['24'],
              }}
            >
              {resolvedBottomLinks.map((item) => (
                <NavigationBottomTabItem
                  key={item.id}
                  item={item}
                  active={item.id === resolvedActiveBottomLinkId}
                  interactionState={interactionState}
                  componentDisabled={componentDisabled}
                  onClick={onBottomLinkClick}
                />
              ))}
            </div>
          </div>

          <button
            type="button"
            disabled={componentDisabled}
            onClick={!componentDisabled ? () => onBottomLinkClick?.('help') : undefined}
            aria-disabled={componentDisabled || undefined}
            style={{
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center',
              gap: spacing.scale['6'],
              borderStyle: 'solid',
              borderWidth: border.width['0'],
              backgroundColor: palette.base.transparent,
              paddingInline: spacing.scale['0'],
              paddingBlock: spacing.scale['0'],
              color: helpTextColor,
              cursor: componentDisabled ? 'default' : 'pointer',
            }}
          >
            <span
              style={{
                whiteSpace: 'nowrap',
                textAlign: 'center',
                ...toTypographyStyle(typography.scale.captionL.medium),
              }}
            >
              {helpLabel}
            </span>
            <IconArrowDownSLine
              aria-hidden
              style={{ width: ICON_SIZE, height: ICON_SIZE, display: 'block' }}
            />
          </button>
        </div>
      </header>
    );
  }

  if (type === '05') {
    return (
      <header
        id={id}
        className={className}
        aria-disabled={componentDisabled || undefined}
        style={{
          ...commonRootStyle,
          minHeight: NAV_HEIGHT,
          display: 'flex',
          alignItems: 'center',
          gap: spacing.scale['32'],
          paddingLeft: spacing.scale['24'],
          paddingRight: spacing.scale['64'],
          paddingTop: spacing.scale['16'],
          paddingBottom: spacing.scale['16'],
          borderBottomStyle: 'solid',
          borderBottomWidth: border.width['1'],
          borderBottomColor: componentDisabled ? palette.gray['2'] : palette.gray['3'],
          backgroundColor: palette.base.white,
          boxSizing: 'border-box',
          ...style,
        }}
        {...props}
      >
        <div
          style={{
            display: 'flex',
            flex: '1 0 0',
            alignItems: 'center',
            minWidth: spacing.scale['0'],
          }}
        >
          <LogoMark />
        </div>

        <BaseCenterLinks
          links={resolvedMainLinks}
          interactionState={interactionState}
          componentDisabled={componentDisabled}
          onLinkClick={onLinkClick}
          justify="center"
        />

        <div
          style={{
            display: 'flex',
            flex: '1 0 0',
            minWidth: spacing.scale['0'],
            minHeight: spacing.scale['0'],
          }}
        />
      </header>
    );
  }

  if (type === '06') {
    return (
      <header
        id={id}
        className={className}
        aria-disabled={componentDisabled || undefined}
        style={{
          ...commonRootStyle,
          minHeight: NAV_HEIGHT,
          display: 'flex',
          alignItems: 'center',
          gap: spacing.scale['32'],
          paddingLeft: spacing.scale['24'],
          paddingRight: spacing.scale['64'],
          paddingTop: spacing.scale['16'],
          paddingBottom: spacing.scale['16'],
          borderBottomStyle: 'solid',
          borderBottomWidth: border.width['1'],
          borderBottomColor: componentDisabled ? palette.gray['2'] : palette.gray['3'],
          backgroundColor: palette.base.white,
          boxSizing: 'border-box',
          ...style,
        }}
        {...props}
      >
        <LogoMark />

        <div
          style={{
            display: 'flex',
            flex: '1 0 0',
            alignItems: 'center',
            gap: spacing.scale['24'],
            minWidth: spacing.scale['0'],
          }}
        >
          <SearchField
            placeholder={searchPlaceholder}
            shortcutLabel={searchShortcutLabel}
            interactionState={interactionState}
            disabled={componentDisabled}
          />

          {resolvedMainLinks.map((item) => (
            <NavigationMainItem
              key={item.id}
              item={item}
              interactionState={interactionState}
              componentDisabled={componentDisabled}
              onClick={onLinkClick}
            />
          ))}
        </div>
      </header>
    );
  }

  return (
    <header
      id={id}
      className={className}
      aria-disabled={componentDisabled || undefined}
      style={{
        ...commonRootStyle,
        minHeight: NAV_HEIGHT,
        display: 'flex',
        alignItems: 'center',
        gap: spacing.scale['32'],
        paddingLeft: spacing.scale['24'],
        paddingRight: spacing.scale['64'],
        paddingTop: spacing.scale['16'],
        paddingBottom: spacing.scale['16'],
        borderBottomStyle: 'solid',
        borderBottomWidth: border.width['1'],
        borderBottomColor: componentDisabled ? palette.gray['2'] : palette.gray['3'],
        backgroundColor: palette.base.white,
        boxSizing: 'border-box',
        ...style,
      }}
      {...props}
    >
      <LogoMark />

      <BaseCenterLinks
        links={resolvedMainLinks}
        interactionState={interactionState}
        componentDisabled={componentDisabled}
        onLinkClick={onLinkClick}
        justify="flex-start"
      />
    </header>
  );
}

API Reference

Props 문서 준비 중입니다. (component-spec.json 추가 시 표시됩니다.)