{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-scroller",
  "title": "MessageScroller",
  "description": "shadcn/ui(new-york-v4) message-scroller의 유니버설(웹+네이티브 한 벌 소스) 포팅. (사이클 40, T-3d). 원본: shadcn-ref/ui/message-scroller.tsx 포팅 판단(원본 대비 차이·이유):",
  "dependencies": [
    "@unikit-bridge/ui"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/message-scroller.tsx",
      "content": "// MessageScroller — shadcn/ui（new-york-v4） message-scroller.tsx 유니버설 포팅\n// （사이클 40, T-3d）. 원본: shadcn-ref/ui/message-scroller.tsx\n//\n// 포팅 판단（원본 대비 차이이유）:\n// - 원본은 헤드리스 엔진 \"@shadcn/react/message-scroller\"（Provider/Root/\n//   Viewport/Content/Item/Button + 훅 3종）을 감싼 얇은 스타일 층이다. 본\n//   포팅은 엔진 없이 브리지 ScrollView를 직접 소비해 재구성했다（설계 확정）.\n// - Viewport = 브리지 ScrollView. ref.scrollTo로 시작/끝 이동을 재현 —\n//   \"끝\"은 매우 큰 y（1e9） 지정으로 양쪽 모두 끝까지 클램프되는 동작을 쓴다.\n// - 자동 하단 이동: Viewport가 children 수（React.Children.count） 변화를\n//   감지해 Provider의 stickToEnd가 true면 끝으로 scrollTo한다（최초 마운트는\n//   비애니메이션）. 원본의 \"끝 근처 여부\" 판단은 onScroll 계약이 무인자라\n//   스크롤 오프셋을 읽을 수 없어 재현 불가 — 소비자가 setStickToEnd（false）로\n//   끊는 구조로 순화했다（한계）.\n// - useMessageScrollerVisibility의 자동 가시성 계산도 같은 계약 한계로\n//   불가 — { atStart, atEnd, setVisibility }를 노출해 소비자가 onScroll에서\n//   판단갱신하는 구조로 순화. 기본값은 채팅 관례（맨 아래） atEnd=true.\n// - useMessageScrollerScrollable（콘텐츠가 뷰포트보다 큰지）은 콘텐츠 크기\n//   측정 계약이 없어 항상 true를 반환하는 스텝이다（원본 API 유지）.\n// - MessageScrollerButton: 기본 children은 브리지 ArrowDownIcon/ArrowUpIcon\n//   （lucide 경유）. sr-only 대신 accessibilityLabel 사용. 원본 icon-sm\n//   사이즈render prop은 없어 Button（secondary） + size-8 토큰으로 근사.\n//   커스텀 이징（임의값）은 제거 — 표준 transition만（네이티브은 즉시 전환）.\n// - MessageScrollerItem의 scrollAnchorcontent-visibility（임의값성능\n//   최적화）은 계약 밖이라 제거 — 순수 레이아웃 토큰만 유지.\n// - scroll-fade-bscrollbar-thin 등 커스텀 유틸과 data-autoscrolling:*\n//   셀렉터는 정의가 없어 제거. overscroll-contain은 웹 전용 → web: 접두.\nimport {\n  Children,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  ArrowDownIcon,\n  ArrowUpIcon,\n  ScrollView,\n  View,\n  type BridgePressableRef,\n  type BridgeScrollViewRef,\n  type BridgeViewRef,\n  type ScrollViewProps,\n  type ViewProps,\n} from \"@unikit-bridge/ui/primitives\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button, type ButtonProps } from \"./button\";\n\n/** scrollTo \"끝\" 근사 — 양 플랫폼 모두 콘텐츠 끝까지 클램프되는 충분히 큰 값 */\nconst SCROLL_END_OFFSET = 1_000_000_000;\n\nexport type MessageScrollerScrollOptions = {\n  x?: number;\n  y?: number;\n  animated?: boolean;\n};\n\nexport type MessageScrollerVisibilityValue = {\n  atStart: boolean;\n  atEnd: boolean;\n};\n\n// Provider가 제공하는 상태 — 스크롤 제어의 단일 출처（원본 헤드리스 엔진 대리）.\ntype MessageScrollerContextValue = {\n  scrollTo: (options: MessageScrollerScrollOptions) => void;\n  scrollToStart: (animated?: boolean) => void;\n  scrollToEnd: (animated?: boolean) => void;\n  /** 끝 고정（새 항목이 오면 자동 하단 이동） — 소비자가 onScroll 판단으로 제어 */\n  stickToEnd: boolean;\n  setStickToEnd: (value: boolean) => void;\n  atStart: boolean;\n  atEnd: boolean;\n  /** 가시성（시작/끝 버튼 표시 판단） 갱신 — 계약상 자동 계산 불가 */\n  setVisibility: (visibility: Partial<MessageScrollerVisibilityValue>) => void;\n  /** Viewport가 자신의 스크롤 ref를 등록하는 통로 */\n  registerViewport: (ref: BridgeScrollViewRef | null) => void;\n};\n\n// Provider 없이 사용했을 때의 안전 폴백（모두 no-op）.\nconst NOOP_SCROLLER: MessageScrollerContextValue = {\n  scrollTo: () => {},\n  scrollToStart: () => {},\n  scrollToEnd: () => {},\n  stickToEnd: true,\n  setStickToEnd: () => {},\n  atStart: false,\n  atEnd: true,\n  setVisibility: () => {},\n  registerViewport: () => {},\n};\n\nconst MessageScrollerContext =\n  createContext<MessageScrollerContextValue>(NOOP_SCROLLER);\n\n// ---------------------------------------------------------------------------\n// MessageScrollerProvider — 스크롤 상태 제공자（원본 Primitive.Provider 대리）.\n// 원본은 props 통과형이었으나 엔진이 없어 상태를 직접 소유한다.\n// ---------------------------------------------------------------------------\nexport type MessageScrollerProviderProps = {\n  children?: ReactNode;\n};\n\nfunction MessageScrollerProvider({ children }: MessageScrollerProviderProps) {\n  const viewportRef = useRef<BridgeScrollViewRef | null>(null);\n  const [stickToEnd, setStickToEnd] = useState(true);\n  // 기본값은 채팅 관례 — 맨 아래에서 시작（끝 버튼 숨김）.\n  const [visibility, setVisibilityState] = useState<MessageScrollerVisibilityValue>(\n    { atStart: false, atEnd: true },\n  );\n  const value = useMemo<MessageScrollerContextValue>(\n    () => ({\n      scrollTo: (options) => viewportRef.current?.scrollTo(options),\n      scrollToStart: (animated = true) =>\n        viewportRef.current?.scrollTo({ y: 0, animated }),\n      scrollToEnd: (animated = true) =>\n        viewportRef.current?.scrollTo({ y: SCROLL_END_OFFSET, animated }),\n      stickToEnd,\n      setStickToEnd,\n      atStart: visibility.atStart,\n      atEnd: visibility.atEnd,\n      setVisibility: (next) =>\n        setVisibilityState((prev) => ({ ...prev, ...next })),\n      registerViewport: (ref) => {\n        viewportRef.current = ref;\n      },\n    }),\n    [stickToEnd, visibility],\n  );\n  return (\n    <MessageScrollerContext.Provider value={value}>\n      {children}\n    </MessageScrollerContext.Provider>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// MessageScroller — 루트 컨테이너（버튼 오버레이의 배경이 되는 상대 배치）.\n// ---------------------------------------------------------------------------\nexport type MessageScrollerProps = ViewProps;\n\nconst MessageScroller = forwardRef<BridgeViewRef, MessageScrollerProps>(\n  ({ className, ...props }, ref) => (\n    <View\n      ref={ref}\n      className={cn(\n        \"relative flex size-full min-h-0 flex-col overflow-hidden\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nMessageScroller.displayName = \"MessageScroller\";\n\n// ---------------------------------------------------------------------------\n// MessageScrollerViewport — 브리지 ScrollView 직접 소비. children 수 변화에\n// 따른 자동 하단 이동（stickToEnd）을 여기서 수행한다.\n// ---------------------------------------------------------------------------\nexport type MessageScrollerViewportProps = ScrollViewProps;\n\nconst MessageScrollerViewport = forwardRef<\n  BridgeScrollViewRef,\n  MessageScrollerViewportProps\n>(\n  (\n    {\n      className,\n      contentContainerClassName,\n      onScroll,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const scroller = useContext(MessageScrollerContext);\n    // 소비자 ref와 Provider 등록을 한 콜백 ref로 병렬 연결한다.\n    const setViewport = useCallback(\n      (node: BridgeScrollViewRef | null) => {\n        scroller.registerViewport(node);\n        if (typeof ref === \"function\") {\n          ref(node);\n        } else if (ref) {\n          // React 18의 readonly RefObject.current를 쓰기 위해 구조 캐스팅.\n          (ref as { current: BridgeScrollViewRef | null }).current = node;\n        }\n      },\n      [scroller, ref],\n    );\n    // children 수 변화 = 새 메시지 도착으로 간주（원본 오프셋 판단의 근사）.\n    const childCount = Children.count(children);\n    const didMountRef = useRef(false);\n    useEffect(() => {\n      if (scroller.stickToEnd) {\n        // 최초 마운트는 점프, 이후 도착은 부드럽게（원본 자동 스크롤 동작）.\n        scroller.scrollToEnd(!didMountRef.current);\n      }\n      didMountRef.current = true;\n      // scroller 함수들은 viewportRef.current를 읽으므로 최신 상태를 참조한다.\n    }, [childCount, scroller.stickToEnd]); // eslint-disable-line react-hooks/exhaustive-deps\n    return (\n      <ScrollView\n        ref={setViewport}\n        className={cn(\n          \"size-full min-h-0 min-w-0 web:overscroll-contain web:contain-content\",\n          className,\n        )}\n        contentContainerClassName={cn(\n          \"min-h-full\",\n          contentContainerClassName,\n        )}\n        onScroll={onScroll}\n        {...props}\n      >\n        {children}\n      </ScrollView>\n    );\n  },\n);\nMessageScrollerViewport.displayName = \"MessageScrollerViewport\";\n\n// ---------------------------------------------------------------------------\n// MessageScrollerContent — 메시지 목록 본문（간격은 gap-8, 원본 토큰）.\n// ---------------------------------------------------------------------------\nexport type MessageScrollerContentProps = ViewProps;\n\nconst MessageScrollerContent = forwardRef<\n  BridgeViewRef,\n  MessageScrollerContentProps\n>(({ className, ...props }, ref) => (\n  <View\n    ref={ref}\n    className={cn(\"flex min-h-full flex-col gap-8 web:h-max\", className)}\n    {...props}\n  />\n));\nMessageScrollerContent.displayName = \"MessageScrollerContent\";\n\n// ---------------------------------------------------------------------------\n// MessageScrollerItem — 개별 항목（원본 scrollAnchorcontent-visibility 제거）.\n// ---------------------------------------------------------------------------\nexport type MessageScrollerItemProps = ViewProps;\n\nconst MessageScrollerItem = forwardRef<BridgeViewRef, MessageScrollerItemProps>(\n  ({ className, ...props }, ref) => (\n    <View ref={ref} className={cn(\"min-w-0 shrink-0\", className)} {...props} />\n  ),\n);\nMessageScrollerItem.displayName = \"MessageScrollerItem\";\n\n// ---------------------------------------------------------------------------\n// MessageScrollerButton — 시작/끝 이동 버튼. 표시 여부는 context 가시성\n// （atEnd/atStart） 또는 소비자 active prop으로 제어한다.\n// ---------------------------------------------------------------------------\nexport type MessageScrollerButtonProps = ButtonProps & {\n  direction?: \"start\" | \"end\";\n  /** 표시 여부 수동 제어 — 생략 시 context 가시성으로 산출 */\n  active?: boolean;\n};\n\nconst MessageScrollerButton = forwardRef<\n  BridgePressableRef,\n  MessageScrollerButtonProps\n>(\n  (\n    {\n      className,\n      direction = \"end\",\n      active,\n      onPress,\n      accessibilityLabel,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const scroller = useContext(MessageScrollerContext);\n    const isVisible =\n      active ?? (direction === \"end\" ? !scroller.atEnd : !scroller.atStart);\n    return (\n      <Button\n        ref={ref}\n        variant=\"secondary\"\n        accessibilityLabel={\n          accessibilityLabel ??\n          (direction === \"end\" ? \"맨 아래로 스크롤\" : \"맨 위로 스크롤\")\n        }\n        className={cn(\n          \"absolute left-1/2 size-8 -translate-x-1/2 p-0 web:transition-opacity web:duration-200 web:rtl:translate-x-1/2\",\n          direction === \"end\" ? \"bottom-4\" : \"top-4\",\n          isVisible\n            ? \"web:opacity-100\"\n            : \"pointer-events-none web:scale-95 web:opacity-0\",\n          className,\n        )}\n        onPress={() => {\n          onPress?.();\n          if (direction === \"end\") {\n            scroller.scrollToEnd();\n          } else {\n            scroller.scrollToStart();\n          }\n        }}\n        {...props}\n      >\n        {children ??\n          (direction === \"end\" ? (\n            <ArrowDownIcon className=\"size-4\" />\n          ) : (\n            <ArrowUpIcon className=\"size-4\" />\n          ))}\n      </Button>\n    );\n  },\n);\nMessageScrollerButton.displayName = \"MessageScrollerButton\";\n\n// ---------------------------------------------------------------------------\n// 훅 3종 — 원본 export명 유지（하단 export 블록에서 재export）.\n// ---------------------------------------------------------------------------\n/** 스크롤 제어 접근（scrollTo/scrollToStart/scrollToEnd끝 고정 상태） */\nfunction useMessageScroller(): MessageScrollerContextValue {\n  return useContext(MessageScrollerContext);\n}\n\n/**\n * 콘텐츠가 뷰포트보다 큰지（스크롤 가능한지）. 브리지 onScroll 계약이\n * 무인자라 콘텐츠 크기를 측정할 수 없어 항상 true를 반환한다（스텁 —\n * 원본 API 유지, 실제 판단은 소비자 몫）.\n */\nfunction useMessageScrollerScrollable(): boolean {\n  return true;\n}\n\n/** 시작/끝 가시성 — 소비자가 onScroll에서 판단해 setVisibility로 갱신한다. */\nfunction useMessageScrollerVisibility(): MessageScrollerVisibilityValue & {\n  setVisibility: MessageScrollerContextValue[\"setVisibility\"];\n} {\n  const { atStart, atEnd, setVisibility } = useContext(MessageScrollerContext);\n  return { atStart, atEnd, setVisibility };\n}\n\nexport {\n  MessageScrollerProvider,\n  MessageScroller,\n  MessageScrollerViewport,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerButton,\n  useMessageScroller,\n  useMessageScrollerScrollable,\n  useMessageScrollerVisibility,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/message-scroller.tsx"
    }
  ],
  "type": "registry:ui"
}
