{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel",
  "title": "Carousel",
  "description": "shadcn/ui(new-york-v4) carousel의 유니버설(웹+네이티브 한 벌 소스) 포팅. 포팅 판단(원본 대비 차이·이유): 소비한다(웹 overflow div + ref.scrollTo smooth / 네이티브 RN ScrollView",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/carousel.tsx",
      "content": "// Carousel — shadcn/ui（new-york-v4） carousel.tsx 유니버설 포팅 （사이클 40, T-3b）.\n// 원본: shadcn-ref/ui/carousel.tsx\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - embla-carousel-react 제거 — 스크롤 축은 브리지 ScrollView 프리미티브를 직접\n//   소비한다（웹 overflow div + ref.scrollTo smooth / 네이티브 RN ScrollView\n//   horizontal + pagingEnabled）. CarouselContent가 ScrollView（뷰포트）를 렌더하고\n//   CarouselItem이 그 직계 자식이 된다（원본 이중 div 구조와 동일 역할）.\n// - 이전/다음 이동: embla api 대신 ScrollView ref.scrollTo（{x|y, animated}）로\n//   절대 오프셋 이동. 스텝 폭은 \"스크롤 뷰포트 폭（또는 높이） = 한 슬라이드\"\n//   근사（basis-full 기본 전제） — 루트 View를 measureInWindow로 측정한다.\n//   콘텐츠 폭 상한은 CarouselContent가 보고한 항목 수 × 스텝으로 근사한다.\n//   사용자가 CarouselItem에 basis-1/2 등 반응형 폭을 주면 실제 슬라이드 폭과\n//   어긋나는 한계（계약상 스크롤 오프셋콘텐츠 폭 읽기 불가 — 판단 기록）.\n// - canScrollPrev/next: 자체 추적 오프셋 기준 계산. onScroll이 무인자 계약이라\n//   직접 스와이프 후 버튼 press 시 목표 오프셋이 실제와 어긋나 점프할 수\n//   있다（한계 — 스크롤 오프셋 읽기 계약 부재, 브리지 확장 제안은 보고서 참조）.\n// - optspluginssetApi: embla 전용 프로퍼티는 제거. setApi는 근사 api\n//   （{ scrollPrev, scrollNext, canScrollPrev, canScrollNext }）를 전달하는\n//   형태로만 유지한다（CarouselApi 타입도 이 근사 형태로 재정의）.\n// - 키보드 탐색: 원본은 루트 div onKeyDownCapture로 Arrow 좌우를 잡는다.\n//   ViewProps에 onKeyDown 계약이 없어 캐스팅 전개（drawer dragHandlers 관행）로\n//   웹 div에만 도달시키고 네이티브에서는 no-op다. ArrowUp/Down（세로）Home/End\n//   등은 미지원.\n// - 아이콘: ArrowLeftIcon·ArrowRightIcon·ArrowUpIcon·ArrowDownIcon — 브리지 SVG\n//   아이콘 프리미티브（lucide — primitives 경유）. 세로 orientation은 원본의 rotate-90\n//   토큰 대신 위/아래 아이콘 교체로 근사（텍스트 회전은 플랫폼 편차）.\n//   sr-only 라벨은 accessibilityLabel로 대체（dialogsheet 관행）. 원본\n//   role=\"region\"aria-roledescription（carousel/slide）은 BridgeRole에 해당\n//   역할이 없어 accessibilityLabel（\"carousel\"/\"slide\"）로만 근사한다.\n// - 원본 -ml-4/pl-4（수평）-mt-4/pt-4（수직） 간격 토큰은 유지 — 음수 마진은\n//   contentContainerClassName（ScrollView 내부 컨테이너）에, 패딩은 Item에 둔다.\n// - 스냅: pagingEnabled（웹 scroll-snap / RN pagingEnabled） + Item web:snap-start.\n//   웹 snap-smooth와 scrollTo smooth가 공존해 최종 위치는 스냅 포인트로 수렴.\n// - translate-y-1/2-left-12 등 위치 토큰은 원본 유지（uniwind transform 지원은\n//   미보증 — 네이티브에서 버튼이 캐러셀 밖에 그려지는 근사 동작）.\nimport {\n  Children,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type MutableRefObject,\n} from \"react\";\nimport {\n  ArrowDownIcon,\n  ArrowLeftIcon,\n  ArrowRightIcon,\n  ArrowUpIcon,\n  ScrollView,\n  View,\n  measureInWindow,\n  type BridgePressableRef,\n  type BridgeScrollViewRef,\n  type BridgeViewRef,\n  type ScrollViewProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport { Button, type ButtonProps } from \"./button\";\nimport { cn } from \"@/lib/utils\";\n\n// ---------------------------------------------------------------------------\n// 컨텍스트 — 원본 useCarousel과 동일한 소비 API\n// ---------------------------------------------------------------------------\n\n/** 근사 api — 원본（embla api）과 달리 우리 구현이 노출하는 조작만 담는다. */\nexport type CarouselApi = {\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n};\n\ntype CarouselContextValue = {\n  orientation: \"horizontal\" | \"vertical\";\n  /** CarouselContent의 ScrollView ref — Previous/Next가 scrollTo로 소비 */\n  carouselRef: MutableRefObject<BridgeScrollViewRef | null>;\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n  /** CarouselContent가 항목 수를 보고（이동 상한 근사용） */\n  reportItemCount: (count: number) => void;\n};\n\nconst CarouselContext = createContext<CarouselContextValue | null>(null);\n\nfunction useCarousel(): CarouselContextValue {\n  const ctx = useContext(CarouselContext);\n  if (!ctx) {\n    throw new Error(\"useCarousel은 <Carousel> 안에서 사용해야 합니다.\");\n  }\n  return ctx;\n}\n\n// ---------------------------------------------------------------------------\n// Carousel 루트\n// ---------------------------------------------------------------------------\n\nexport type CarouselProps = ViewProps & {\n  orientation?: \"horizontal\" | \"vertical\";\n  /** 근사 api를 상위로 올려주는 콜백（원본 setApi의 축소 형태） */\n  setApi?: (api: CarouselApi) => void;\n};\n\nconst Carousel = forwardRef<BridgeViewRef, CarouselProps>(\n  (\n    { orientation = \"horizontal\", setApi, className, children, ...props },\n    forwardedRef,\n  ) => {\n    const [canScrollPrev, setCanScrollPrev] = useState(false);\n    const [canScrollNext, setCanScrollNext] = useState(false);\n    const carouselRef = useRef<BridgeScrollViewRef | null>(null);\n    // 루트 ref — 뷰포트 폭/높이 측정용（measureInWindow은 요소를 받는다）.\n    const rootRef = useRef<BridgeViewRef | null>(null);\n    // 버튼 이동의 목표 절대 오프셋 추적（실제 스크롤 오프셋 읽기 계약 부재）.\n    const offsetRef = useRef(0);\n    const countRef = useRef(0);\n\n    const setRef = (element: BridgeViewRef | null) => {\n      rootRef.current = element;\n      if (typeof forwardedRef === \"function\") forwardedRef(element);\n      else if (forwardedRef) forwardedRef.current = element;\n    };\n\n    const reportItemCount = useCallback((count: number) => {\n      countRef.current = count;\n    }, []);\n\n    // 방향 이동 — 뷰포트 크기 측정 후 절대 오프셋 scrollTo.\n    const scrollBy = useCallback(\n      (direction: 1 | -1) => {\n        const el = rootRef.current;\n        if (!el) return;\n        measureInWindow(el, (_x, _y, width, height) => {\n          const viewport =\n            orientation === \"horizontal\" ? width : height;\n          if (viewport <= 0) return;\n          // 콘텐츠 폭 근사: 항목 수 × 뷰포트（basis-full 전제） — 끝 슬라이드가\n          // 보이는 지점을 상한으로 쓴다.\n          const max = Math.max((countRef.current - 1) * viewport, 0);\n          const next = Math.min(\n            Math.max(offsetRef.current + direction * viewport, 0),\n            max,\n          );\n          offsetRef.current = next;\n          if (orientation === \"horizontal\") {\n            carouselRef.current?.scrollTo({ x: next, animated: true });\n          } else {\n            carouselRef.current?.scrollTo({ y: next, animated: true });\n          }\n          setCanScrollPrev(next > 0);\n          setCanScrollNext(next < max);\n        });\n      },\n      [orientation],\n    );\n\n    const scrollPrev = useCallback(() => scrollBy(-1), [scrollBy]);\n    const scrollNext = useCallback(() => scrollBy(1), [scrollBy]);\n\n    // 원본과 동일한 ArrowLeft/Right 캡처 — ViewProps에 onKeyDown 계약이 없어\n    // 캐스팅 전개로 웹 div에만 도달시킨다（네이티브 no-op）.\n    const handleKeyDown = useCallback(\n      (event: { key: string; preventDefault: () => void }) => {\n        if (event.key === \"ArrowLeft\") {\n          event.preventDefault();\n          scrollPrev();\n        } else if (event.key === \"ArrowRight\") {\n          event.preventDefault();\n          scrollNext();\n        }\n      },\n      [scrollPrev, scrollNext],\n    );\n\n    useEffect(() => {\n      if (!setApi) return;\n      setApi({ scrollPrev, scrollNext, canScrollPrev, canScrollNext });\n    }, [setApi, scrollPrev, scrollNext, canScrollPrev, canScrollNext]);\n\n    const contextValue = useMemo(\n      () => ({\n        orientation,\n        carouselRef,\n        scrollPrev,\n        scrollNext,\n        canScrollPrev,\n        canScrollNext,\n        reportItemCount,\n      }),\n      [orientation, scrollPrev, scrollNext, canScrollPrev, canScrollNext, reportItemCount],\n    );\n\n    return (\n      <CarouselContext.Provider value={contextValue}>\n        <View\n          ref={setRef}\n          accessibilityLabel=\"carousel\"\n          className={cn(\"relative\", className)}\n          {...props}\n          {...({ onKeyDown: handleKeyDown } as ViewProps)}\n        >\n          {children}\n        </View>\n      </CarouselContext.Provider>\n    );\n  },\n);\nCarousel.displayName = \"Carousel\";\n\n// ---------------------------------------------------------------------------\n// Content — 브리지 ScrollView（가로/세로 축）\n// ---------------------------------------------------------------------------\n\nexport type CarouselContentProps = ScrollViewProps;\n\nconst CarouselContent = forwardRef<BridgeScrollViewRef, CarouselContentProps>(\n  ({ className, children, ...props }, ref) => {\n    const { carouselRef, orientation, reportItemCount } = useCarousel();\n\n    // 항목 수 보고 — 이동 가능 상한 근사의 근거.\n    useEffect(() => {\n      reportItemCount(Children.count(children));\n    }, [children, reportItemCount]);\n\n    const setRef = (element: BridgeScrollViewRef | null) => {\n      carouselRef.current = element;\n      if (typeof ref === \"function\") ref(element);\n      else if (ref) ref.current = element;\n    };\n\n    return (\n      <ScrollView\n        ref={setRef}\n        horizontal={orientation === \"horizontal\"}\n        pagingEnabled\n        // 교차축 overflow는 브리지 ScrollView가 축 전환 시 스스로 잠근다.\n        className={cn(\"relative\", className)}\n        contentContainerClassName={\n          orientation === \"horizontal\" ? \"flex-row -ml-4\" : \"flex-col -mt-4\"\n        }        {...props}\n      >\n        {children}\n      </ScrollView>\n    );\n  },\n);\nCarouselContent.displayName = \"CarouselContent\";\n\n// ---------------------------------------------------------------------------\n// Item — 슬라이드（토큰 폭은 원본 값: basis-full, 소비자가 basis-1/2 등으로 오버라이드）\n// ---------------------------------------------------------------------------\n\nexport type CarouselItemProps = ViewProps;\n\nconst CarouselItem = forwardRef<BridgeViewRef, CarouselItemProps>(\n  ({ className, ...props }, ref) => {\n    const { orientation } = useCarousel();\n    return (\n      <View\n        ref={ref}\n        accessibilityLabel=\"slide\"\n        className={cn(\n          \"min-w-0 shrink-0 grow-0 basis-full web:snap-start\",\n          orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n          className,\n        )}\n        {...props}\n      />\n    );\n  },\n);\nCarouselItem.displayName = \"CarouselItem\";\n\n// ---------------------------------------------------------------------------\n// Previous / Next — Button 재사용 + 글리프\n// ---------------------------------------------------------------------------\n\nexport type CarouselPreviousProps = ButtonProps;\n\nconst CarouselPrevious = forwardRef<BridgePressableRef, CarouselPreviousProps>(\n  ({ className, variant = \"outline\", size = \"icon\", ...props }, ref) => {\n    const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        size={size}\n        accessibilityLabel=\"Previous slide\"\n        disabled={!canScrollPrev}\n        onPress={scrollPrev}\n        className={cn(\n          \"absolute size-8 rounded-full\",\n          orientation === \"horizontal\"\n            ? \"top-1/2 -left-12 -translate-y-1/2\"\n            : \"-top-12 left-1/2 -translate-x-1/2\",\n          className,\n        )}\n        {...props}\n      >\n        {orientation === \"horizontal\" ? (\n          <ArrowLeftIcon className=\"size-4\" />\n        ) : (\n          <ArrowUpIcon className=\"size-4\" />\n        )}\n      </Button>\n    );\n  },\n);\nCarouselPrevious.displayName = \"CarouselPrevious\";\n\nexport type CarouselNextProps = ButtonProps;\n\nconst CarouselNext = forwardRef<BridgePressableRef, CarouselNextProps>(\n  ({ className, variant = \"outline\", size = \"icon\", ...props }, ref) => {\n    const { orientation, scrollNext, canScrollNext } = useCarousel();\n    return (\n      <Button\n        ref={ref}\n        variant={variant}\n        size={size}\n        accessibilityLabel=\"Next slide\"\n        disabled={!canScrollNext}\n        onPress={scrollNext}\n        className={cn(\n          \"absolute size-8 rounded-full\",\n          orientation === \"horizontal\"\n            ? \"top-1/2 -right-12 -translate-y-1/2\"\n            : \"-bottom-12 left-1/2 -translate-x-1/2\",\n          className,\n        )}\n        {...props}\n      >\n        {orientation === \"horizontal\" ? (\n          <ArrowRightIcon className=\"size-4\" />\n        ) : (\n          <ArrowDownIcon className=\"size-4\" />\n        )}\n      </Button>\n    );\n  },\n);\nCarouselNext.displayName = \"CarouselNext\";\n\nexport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/carousel.tsx"
    }
  ],
  "type": "registry:ui"
}
