'use client';/** * @title Bottom Navigation * @description 앱의 루트 화면 하단에 고정되어 최대 다섯 개의 상위 탭 사이를 이동하게 해주는 네비게이션 바예요. */import * as React from 'react';import { Slot } from 'radix-ui';import { cn } from '@/lib/utils';// SEED Design: components/bottom-navigation.mdx (Rootage 스펙 없음 — 디자인 가이드 기준)// - Container: bg-layer-default, 상단 1px divider, 내부 최대 너비 480px// - Item: Icon(24) + Label, Selected/Unselected × Enabled/Disabled// - 폰트 스케일링 미적용(static 글자 토큰), Notification Badge(small/large)type BottomNavigationContextValue = { value?: string; onSelect: (value: string) => void;};const BottomNavigationContext = React.createContext<BottomNavigationContextValue | null>(null);export interface BottomNavigationProps extends Omit<React.ComponentProps<'nav'>, 'defaultValue'> { /** 선택된 탭 값 (제어). */ value?: string; /** 처음 선택된 탭 값 (비제어). */ defaultValue?: string; onValueChange?: (value: string) => void;}function BottomNavigation({ className, value: valueProp, defaultValue, onValueChange, children, ...props}: BottomNavigationProps) { const [uncontrolled, setUncontrolled] = React.useState(defaultValue); const value = valueProp ?? uncontrolled; const onSelect = React.useCallback( (next: string) => { if (valueProp === undefined) setUncontrolled(next); onValueChange?.(next); }, [valueProp, onValueChange], ); return ( <BottomNavigationContext.Provider value={{ value, onSelect }}> <nav data-slot="bottom-navigation" className={cn( 'w-full shrink-0 border-t border-stroke-neutral-muted bg-bg-layer-default pb-[env(safe-area-inset-bottom)]', className, )} {...props} > <div data-slot="bottom-navigation-list" className="mx-auto flex w-full max-w-[480px] items-stretch"> {children} </div> </nav> </BottomNavigationContext.Provider> );}export interface BottomNavigationItemProps extends Omit<React.ComponentProps<'button'>, 'value'> { /** 탭 식별 값. */ value?: string; /** 탭 아이콘. 활성 여부는 색으로 구분하므로 Fill 스타일 아이콘을 권장해요. */ icon: React.ReactNode; /** 탭 라벨 (한글 5자, 영문 10자 이내 권장). */ label: React.ReactNode; /** 선택 상태를 직접 지정해요. 지정하지 않으면 BottomNavigation 의 value 와 비교해요. */ selected?: boolean; /** 'dot' 이면 Small 배지, 숫자면 Large 배지 (0 은 숨김, 100 이상은 99+). */ badge?: 'dot' | number; asChild?: boolean;}function BottomNavigationItem({ className, value, icon, label, selected: selectedProp, badge, asChild = false, disabled, onClick, children, type, ...props}: BottomNavigationItemProps) { const ctx = React.useContext(BottomNavigationContext); const selected = selectedProp ?? (value !== undefined && ctx?.value === value); const Comp = asChild ? Slot.Root : 'button'; const content = ( <> <span data-slot="bottom-navigation-icon" className="relative inline-flex size-[24px] items-center justify-center"> {icon} <BottomNavigationBadge badge={badge} /> </span> <span data-slot="bottom-navigation-label" className="max-w-full truncate text-t1-static font-medium"> {label} </span> </> ); return ( <Comp data-slot="bottom-navigation-item" data-selected={selected || undefined} data-disabled={disabled || undefined} aria-current={selected ? 'page' : undefined} type={asChild ? undefined : (type ?? 'button')} disabled={disabled} onClick={(e: React.MouseEvent<HTMLButtonElement>) => { onClick?.(e); if (!e.defaultPrevented && value !== undefined && !disabled) ctx?.onSelect(value); }} className={cn( 'relative flex min-w-0 flex-1 cursor-pointer select-none flex-col items-center justify-center gap-x1 pt-x2 pb-x1_5', 'text-fg-neutral-subtle data-selected:text-fg-neutral', 'transition-[color,scale,outline-color] duration-(--rb-duration-color-transition) ease-easing', 'outline-2 -outline-offset-2 outline-transparent focus-visible:outline-stroke-focus-ring', 'active:scale-[0.95] data-disabled:active:scale-100', 'disabled:cursor-not-allowed disabled:text-fg-disabled data-disabled:cursor-not-allowed data-disabled:text-fg-disabled', '[&_svg]:pointer-events-none [&_svg]:size-[24px] [&_svg]:shrink-0', className, )} {...props} > {asChild ? ( React.isValidElement<{ children?: React.ReactNode }>(children) ? ( React.cloneElement(children, undefined, content) ) : ( children ) ) : ( content )} </Comp> );}// SEED Rootage spec: notification-badge.yaml (icon attached)function BottomNavigationBadge({ badge }: { badge?: 'dot' | number }) { if (badge === undefined || badge === 0) return null; if (badge === 'dot') { return ( <span data-slot="bottom-navigation-badge" data-size="small" aria-hidden className="absolute -top-px -right-px size-[6px] rounded-full bg-bg-brand-solid" /> ); } return ( <span data-slot="bottom-navigation-badge" data-size="large" className="absolute -top-x1 left-[14px] inline-flex min-h-[18px] min-w-[18px] items-center justify-center rounded-full bg-bg-brand-solid px-x1 text-t1-static font-bold text-fg-on-brand-solid" > {badge > 99 ? '99+' : badge} </span> );}export { BottomNavigation, BottomNavigationItem };