한 화면의 콘텐츠를 탭 단위로 구분하고, 탭 선택이나 좌우 스와이프로 전환하는 컴포넌트입니다.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
npx @seed-design/cli add ui:tabs pnpm dlx @seed-design/cli add ui:tabs yarn dlx @seed-design/cli add ui:tabs bun x @seed-design/cli add ui:tabs
의존성 설치
npm install @seed-design/lynx-react yarn add @seed-design/lynx-react pnpm add @seed-design/lynx-react bun add @seed-design/lynx-react 아래 코드를 복사 후 붙여넣고 사용하세요 /**
* @file ui:tabs
* @requires @seed-design/lynx-react@>=0.1.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.1.0 <1.0.0
**/
import {
NotificationBadge,
NotificationBadgePositioner,
Tabs as SeedTabs,
type TabsCarouselProps as SeedTabsCarouselProps,
type TabsContentProps as SeedTabsContentProps,
type TabsListProps as SeedTabsListProps,
type TabsRootProps as SeedTabsRootProps,
type TabsTriggerProps as SeedTabsTriggerProps,
} from "@seed-design/lynx-react" ;
import { forwardRef } from "@lynx-js/react" ;
export interface TabsRootProps extends SeedTabsRootProps {}
/**
* @see https://seed-design.io/lynx/components/tabs
*/
export const TabsRoot = forwardRef < unknown , TabsRootProps >(( props , ref ) => {
return < SeedTabs.Root ref = {ref} { ... props} />;
});
TabsRoot.displayName = "TabsRoot" ;
export interface TabsListProps extends SeedTabsListProps {}
export const TabsList = forwardRef < unknown , TabsListProps >(( props , ref ) => {
const { children , ... otherProps } = props;
return (
< SeedTabs.List ref = {ref} { ... otherProps}>
{children}
< SeedTabs.Indicator />
</ SeedTabs.List >
);
});
TabsList.displayName = "TabsList" ;
export interface TabsTriggerProps extends Omit < SeedTabsTriggerProps , "notification" > {
notification ?: boolean ;
}
export const TabsTrigger = forwardRef < unknown , TabsTriggerProps >(( props , ref ) => {
const { children , notification , ... otherProps } = props;
return (
< SeedTabs.Trigger
ref = {ref}
{ ... otherProps}
notification = {
notification ? (
< NotificationBadgePositioner size = "small" attach = "text" >
< NotificationBadge accessibility-elements-hidden = { true } />
</ NotificationBadgePositioner >
) : undefined
}
>
{children}
</ SeedTabs.Trigger >
);
});
TabsTrigger.displayName = "TabsTrigger" ;
export interface TabsCarouselProps extends SeedTabsCarouselProps {}
export const TabsCarousel = forwardRef < unknown , TabsCarouselProps >(( props , ref ) => {
const { children , ... otherProps } = props;
return (
< SeedTabs.Carousel ref = {ref} { ... otherProps}>
< SeedTabs.CarouselCamera >{children}</ SeedTabs.CarouselCamera >
</ SeedTabs.Carousel >
);
});
TabsCarousel.displayName = "TabsCarousel" ;
export interface TabsContentProps extends SeedTabsContentProps {}
export const TabsContent = SeedTabs.Content;
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
value?string | undefined
defaultValue?string | undefined
onValueChange?(( value : string ) => void ) | undefined
contentLayout?"fill" | "hug" | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
notification?boolean | undefined
disabled?boolean | undefined
className?string | undefined
bindtap?EventHandler < BaseTouchEvent < Target >> | undefined
childrenstring | number
style?CSSProperties | undefined
valuestring
accessibility-label?string | undefined
swipeable?boolean | undefined
onSettle?(() => void ) | undefined
onSwipeStart?(() => void ) | undefined
onSwipeEnd?(() => void ) | undefined
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
valuestring
style?CSSProperties | undefined
children?ReactNode
className?string | undefined
import {
TabsCarousel,
TabsContent,
TabsList,
TabsRoot,
TabsTrigger,
} from "@/components/ui/tabs" ;
export function App () {
return (
< TabsRoot defaultValue = "one" triggerLayout = "fill" >
< TabsList >
< TabsTrigger value = "one" >첫 번째</ TabsTrigger >
< TabsTrigger value = "two" >두 번째</ TabsTrigger >
</ TabsList >
< TabsCarousel swipeable className = "tabs-carousel" >
< TabsContent value = "one" >
< text >첫 번째 콘텐츠</ text >
</ TabsContent >
< TabsContent value = "two" >
< text >두 번째 콘텐츠</ text >
</ TabsContent >
</ TabsCarousel >
</ TabsRoot >
);
}
Registry의 TabsList는 Tabs.Indicator를 자동으로 추가합니다. TabsCarousel도 내부에서 Tabs.CarouselCamera를 조립하므로 각 TabsContent만 자식으로 전달하면 됩니다.
TabsCarousel은 Lynx 네이티브 <viewpager>를 사용합니다. Carousel에 앱 레이아웃에 맞는 높이를 지정해야 하며, swipeable을 설정하면 좌우 스와이프로 탭을 바꿀 수 있습니다.
iOS에서는 화면 왼쪽 32px 안에서 시작하는 뒤로가기 제스처를 우선합니다. 앱의 제스처 영역과 맞춰야 한다면 iosBackGestureEdgeWidth로 너비를 조정하세요.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "2" triggerLayout = "fill" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" triggerLayout = "hug" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" size = "medium" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" size = "small" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
Lynx의 TabsCarousel은 탭을 선택할 때 native viewpager의 selectTab을 smooth: false로 호출하므로 콘텐츠가 즉시 바뀝니다. 전환 효과가 필요하면 TabsCarousel 대신 선택 값을 기준으로 콘텐츠를 직접 렌더링하고 앱에서 애니메이션을 적용하세요.
Trigger를 탭하거나 콘텐츠를 좌우로 밀면 선택 값과 Indicator가 함께 이동합니다.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsCarousel, TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "2" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsCarousel swipeable className = "tabs-preview__carousel" >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsCarousel >
</ TabsRoot >
</ view >
</ view >
);
}
Lynx는 carouselPreventDrag를 지원하지 않습니다. Carousel 안에서 드래그와 충돌하는 조작 요소는 Carousel 밖에 배치하세요.
disabled Trigger는 탭할 수 없습니다. Carousel이 비활성 탭에 도달하면 다음 활성 탭으로 이동합니다.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" disabled >
라벨2
</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" notification >
라벨2
</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ view >
</ view >
);
}
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< scroll-view className = "tabs-preview__scroll" scroll-orientation = "vertical" >
< TabsRoot defaultValue = "1" size = "medium" stickyList >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent className = "tabs-preview__sticky-content" value = "1" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__sticky-content" value = "2" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent className = "tabs-preview__sticky-content" value = "3" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
</ scroll-view >
</ view >
);
}
이 구성에서는 Tabs가 제공하는 스와이프 기능을 사용할 수 없습니다.
import "./styles" ;
import { useState } from "@lynx-js/react" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ activeTab , setActiveTab ] = useState ( "1" );
function handleValueChange ( value : string ) {
"background only" ;
setActiveTab (value);
}
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot defaultValue = "1" onValueChange = {handleValueChange}>
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
{activeTab === "1" && (
< view className = "tabs-preview__content" >
< text className = "tabs-preview__content-text" >Content 1</ text >
</ view >
)}
{activeTab === "2" && (
< view className = "tabs-preview__content" >
< text className = "tabs-preview__content-text" >Content 2</ text >
</ view >
)}
{activeTab === "3" && (
< view className = "tabs-preview__content" >
< text className = "tabs-preview__content-text" >Content 3</ text >
</ view >
)}
</ TabsRoot >
</ view >
</ view >
);
}
TabsCarousel의 autoHeight, lazyMount, unmountOnExit 없이도 TabsContent를 Carousel 밖에 배치하면 선택된 콘텐츠 높이에 맞춰 아래 콘텐츠의 위치를 바꿀 수 있습니다. 이 구성은 스와이프를 제공하지 않습니다.
import "./styles" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview tabs-preview--dynamic-height" >
< TabsRoot defaultValue = "1" >
< TabsList >
< TabsTrigger value = "1" >라벨1</ TabsTrigger >
< TabsTrigger value = "2" >라벨2</ TabsTrigger >
< TabsTrigger value = "3" >라벨3</ TabsTrigger >
</ TabsList >
< TabsContent
className = "tabs-preview__dynamic-content"
value = "1"
style = {{ height: "100px" }}
>
< text className = "tabs-preview__content-text" >Content 1</ text >
</ TabsContent >
< TabsContent
className = "tabs-preview__dynamic-content"
value = "2"
style = {{ height: "200px" }}
>
< text className = "tabs-preview__content-text" >Content 2</ text >
</ TabsContent >
< TabsContent
className = "tabs-preview__dynamic-content"
value = "3"
style = {{ height: "300px" }}
>
< text className = "tabs-preview__content-text" >Content 3</ text >
</ TabsContent >
</ TabsRoot >
< view className = "tabs-preview__dynamic-footer" >
< text className = "tabs-preview__content-text" >아래 콘텐츠</ text >
</ view >
</ view >
</ view >
);
}
TabsTrigger의 bindtap은 이미 선택된 Trigger를 다시 탭할 때도 호출됩니다. 아래 예시는 네 번째 항목부터 보이도록 시작하며, 선택된 탭을 다시 탭하면 현재 콘텐츠의 scroll-view가 맨 위로 이동합니다.
import "./styles" ;
import { useCallback, useRef, useState } from "@lynx-js/react" ;
import type { NodesRef } from "@lynx-js/types" ;
import { useSeedClassName } from "@seed-design/lynx-react" ;
import { TabsCarousel, TabsContent, TabsList, TabsRoot, TabsTrigger } from "@/components/ui/tabs" ;
const SCROLL_ITEMS = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ];
function scrollToTop ( ref : NodesRef | null ) {
"background only" ;
ref?. invoke ({ method: "scrollTo" , params: { offset: 0 , smooth: true } }). exec ();
}
export default function Example () {
const seedClassName = useSeedClassName ({ colorMode: "system" });
const [ currentTab , setCurrentTab ] = useState ( "1" );
const firstContentRef = useRef < NodesRef | null >( null );
const secondContentRef = useRef < NodesRef | null >( null );
const setFirstContentRef = useCallback (( node : NodesRef | null ) => {
"background only" ;
firstContentRef.current = node;
}, []);
const setSecondContentRef = useCallback (( node : NodesRef | null ) => {
"background only" ;
secondContentRef.current = node;
}, []);
function handleFirstTriggerTap () {
"background only" ;
if (currentTab === "1" ) scrollToTop (firstContentRef.current);
}
function handleSecondTriggerTap () {
"background only" ;
if (currentTab === "2" ) scrollToTop (secondContentRef.current);
}
return (
< view className = { `${ seedClassName } docs-lynx-tabs-root` }>
< view className = "tabs-preview" >
< TabsRoot triggerLayout = "fill" value = {currentTab} onValueChange = {setCurrentTab}>
< TabsList >
< TabsTrigger value = "1" bindtap = {handleFirstTriggerTap}>
라벨1
</ TabsTrigger >
< TabsTrigger value = "2" bindtap = {handleSecondTriggerTap}>
라벨2
</ TabsTrigger >
</ TabsList >
< TabsCarousel swipeable className = "tabs-preview__scroll-top-carousel" >
< TabsContent value = "1" >
< scroll-view
ref = {setFirstContentRef}
scroll-orientation = "vertical"
initial-scroll-offset = { 300 }
className = "tabs-preview__scroll-top"
>
< view className = "tabs-preview__scroll-top-content" >
{ SCROLL_ITEMS . map (( item ) => (
< view key = {item} className = "tabs-preview__scroll-top-item" >
< text className = "tabs-preview__content-text" >Content 1 · {item}</ text >
</ view >
))}
</ view >
</ scroll-view >
</ TabsContent >
< TabsContent value = "2" >
< scroll-view
ref = {setSecondContentRef}
scroll-orientation = "vertical"
initial-scroll-offset = { 300 }
className = "tabs-preview__scroll-top"
>
< view className = "tabs-preview__scroll-top-content" >
{ SCROLL_ITEMS . map (( item ) => (
< view key = {item} className = "tabs-preview__scroll-top-item" >
< text className = "tabs-preview__content-text" >Content 2 · {item}</ text >
</ view >
))}
</ view >
</ scroll-view >
</ TabsContent >
</ TabsCarousel >
</ TabsRoot >
</ view >
</ view >
);
}
항목 React Web Lynx 렌더링 요소 HTML div, button 네이티브 view, text, scroll-view, viewpager 탭 선택 이벤트 onClick 기반bindtap 기반, 공개 상태 이벤트는 onValueChangeCarousel Embla Carousel 네이티브 <viewpager> 가로 List CSS overflow 네이티브 <scroll-view> 제어 방식 value, defaultValue동일 접근성 ARIA tablist, tab, tabpanel Lynx accessibility-* 속성 Indicator DOM 측정과 CSS 변수 bindlayoutchange 측정과 viewpager offset 보간
현재 Lynx 구현은 다음 Web 기능을 지원하지 않습니다.
세로 orientation
키보드 방향키, Home/End, roving focus
RTL 순서와 스와이프 방향 반전
asChild
lazyMount, unmountOnExit
Carousel의 loop, autoHeight, dragThreshold
carouselPreventDrag
콘텐츠 전환 애니메이션
Lynx의 native viewpager는 page slot을 유지해야 하므로 Carousel 콘텐츠를 lazy unmount하지 않습니다.