{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-scroller",
  "type": "registry:ui",
  "title": "Message Scroller",
  "description": "Message scroller for Tile UI.",
  "devDependencies": [
    "sass"
  ],
  "registryDependencies": [
    "@tile-ui/core",
    "@tile-ui/styles"
  ],
  "files": [
    {
      "path": "packages/react/src/components/message-scroller/message-scroller.tsx",
      "type": "registry:ui",
      "target": "components/ui/message-scroller/message-scroller.tsx",
      "content": "import React, { createContext, useContext, useEffect, useRef, useState } from 'react';\nimport { getMessageScrollerButtonStyleKeys, isScrollerNearBottom, messageScrollerStyleKeys, scrollScrollerToEnd, scrollScrollerToStart } from '../lib/core';\nimport type { MessageScrollerButtonBaseProps, MessageScrollerItemBaseProps } from '../lib/core';\nimport styles from './message-scroller.module.scss';\n\ninterface MessageScrollerContextValue {\n\tviewportRef: React.RefObject<HTMLElement | null>;\n\tcontentRef: React.RefObject<HTMLElement | null>;\n\tbuttonActive: boolean;\n\tscrollable: boolean;\n\tregisterViewport: (element: HTMLElement | null) => void;\n\tregisterContent: (element: HTMLElement | null) => void;\n\thandleViewportScroll: () => void;\n\tscrollToEnd: (behavior?: ScrollBehavior) => void;\n\tscrollToStart: (behavior?: ScrollBehavior) => void;\n}\n\nconst MessageScrollerContext = createContext<MessageScrollerContextValue | null>(null);\n\nfunction useMessageScrollerContext(): MessageScrollerContextValue {\n\tconst context = useContext(MessageScrollerContext);\n\tif (!context) {\n\t\tthrow new Error('MessageScroller sub-components must be used within <MessageScrollerProvider>.');\n\t}\n\treturn context;\n}\n\nexport interface MessageScrollerProviderProps extends React.PropsWithChildren {}\n\nfunction MessageScrollerProvider({ children }: MessageScrollerProviderProps) {\n\tconst viewportRef = useRef<HTMLElement | null>(null);\n\tconst contentRef = useRef<HTMLElement | null>(null);\n\tconst stickToBottomRef = useRef(true);\n\tconst [buttonActive, setButtonActive] = useState(false);\n\tconst [scrollable, setScrollable] = useState(false);\n\n\tfunction registerViewport(element: HTMLElement | null) {\n\t\tviewportRef.current = element;\n\t\tif (element) {\n\t\t\tupdateScrollState(element);\n\t\t}\n\t}\n\n\tfunction registerContent(element: HTMLElement | null) {\n\t\tcontentRef.current = element;\n\t}\n\n\tfunction updateScrollState(viewport: HTMLElement) {\n\t\tconst isScrollable = viewport.scrollHeight > viewport.clientHeight;\n\t\tconst nearBottom = isScrollerNearBottom(viewport);\n\t\tstickToBottomRef.current = nearBottom;\n\t\tsetScrollable(isScrollable);\n\t\tsetButtonActive(isScrollable && !nearBottom);\n\t}\n\n\tfunction handleViewportScroll() {\n\t\tconst viewport = viewportRef.current;\n\t\tif (viewport) {\n\t\t\tupdateScrollState(viewport);\n\t\t}\n\t}\n\n\tfunction scrollToEnd(behavior: ScrollBehavior = 'smooth') {\n\t\tconst viewport = viewportRef.current;\n\t\tif (!viewport) {\n\t\t\treturn;\n\t\t}\n\t\tstickToBottomRef.current = true;\n\t\tscrollScrollerToEnd(viewport, behavior);\n\t\tsetButtonActive(false);\n\t}\n\n\tfunction scrollToStart(behavior: ScrollBehavior = 'smooth') {\n\t\tconst viewport = viewportRef.current;\n\t\tif (!viewport) {\n\t\t\treturn;\n\t\t}\n\t\tstickToBottomRef.current = false;\n\t\tscrollScrollerToStart(viewport, behavior);\n\t}\n\n\tuseEffect(() => {\n\t\tconst content = contentRef.current;\n\t\tif (!content) {\n\t\t\treturn;\n\t\t}\n\t\tconst observer = new ResizeObserver(() => {\n\t\t\tconst viewport = viewportRef.current;\n\t\t\tif (!viewport) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tupdateScrollState(viewport);\n\t\t\tif (stickToBottomRef.current) {\n\t\t\t\tscrollScrollerToEnd(viewport, 'auto');\n\t\t\t}\n\t\t});\n\t\tobserver.observe(content);\n\t\treturn () => observer.disconnect();\n\t}, []);\n\n\treturn (\n\t\t<MessageScrollerContext.Provider\n\t\t\tvalue={{ viewportRef, contentRef, buttonActive, scrollable, registerViewport, registerContent, handleViewportScroll, scrollToEnd, scrollToStart }}>\n\t\t\t{children}\n\t\t</MessageScrollerContext.Provider>\n\t);\n}\nMessageScrollerProvider.displayName = 'MessageScrollerProvider';\n\nexport interface MessageScrollerProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst MessageScroller = React.forwardRef<HTMLDivElement, MessageScrollerProps>(({ className = '', children, ...props }, ref) => {\n\treturn (\n\t\t<div ref={ref} data-slot=\"message-scroller\" className={[styles[messageScrollerStyleKeys.root], className].filter(Boolean).join(' ')} {...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nMessageScroller.displayName = 'MessageScroller';\n\nexport interface MessageScrollerViewportProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst MessageScrollerViewport = React.forwardRef<HTMLDivElement, MessageScrollerViewportProps>(({ className = '', children, onScroll, ...props }, ref) => {\n\tconst context = useMessageScrollerContext();\n\n\tfunction setRef(element: HTMLDivElement | null) {\n\t\tcontext.registerViewport(element);\n\t\tif (typeof ref === 'function') {\n\t\t\tref(element);\n\t\t} else if (ref) {\n\t\t\tref.current = element;\n\t\t}\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tref={setRef}\n\t\t\ttabIndex={0}\n\t\t\tdata-slot=\"message-scroller-viewport\"\n\t\t\tclassName={[styles[messageScrollerStyleKeys.viewport], className].filter(Boolean).join(' ')}\n\t\t\tonScroll={(event) => {\n\t\t\t\tcontext.handleViewportScroll();\n\t\t\t\tonScroll?.(event);\n\t\t\t}}\n\t\t\t{...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nMessageScrollerViewport.displayName = 'MessageScrollerViewport';\n\nexport interface MessageScrollerContentProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst MessageScrollerContent = React.forwardRef<HTMLDivElement, MessageScrollerContentProps>(({ className = '', children, ...props }, ref) => {\n\tconst context = useMessageScrollerContext();\n\n\tfunction setRef(element: HTMLDivElement | null) {\n\t\tcontext.registerContent(element);\n\t\tif (typeof ref === 'function') {\n\t\t\tref(element);\n\t\t} else if (ref) {\n\t\t\tref.current = element;\n\t\t}\n\t}\n\n\treturn (\n\t\t<div ref={setRef} data-slot=\"message-scroller-content\" className={[styles[messageScrollerStyleKeys.content], className].filter(Boolean).join(' ')} {...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nMessageScrollerContent.displayName = 'MessageScrollerContent';\n\nexport interface MessageScrollerItemProps extends React.HTMLAttributes<HTMLDivElement>, MessageScrollerItemBaseProps {}\n\nconst MessageScrollerItem = React.forwardRef<HTMLDivElement, MessageScrollerItemProps>(({ className = '', scrollAnchor = false, children, ...props }, ref) => {\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\tdata-slot=\"message-scroller-item\"\n\t\t\tdata-scroll-anchor={scrollAnchor}\n\t\t\tclassName={[styles[messageScrollerStyleKeys.item], className].filter(Boolean).join(' ')}\n\t\t\t{...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nMessageScrollerItem.displayName = 'MessageScrollerItem';\n\nfunction ArrowDownIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<path d=\"M12 5v14\" />\n\t\t\t<path d=\"m19 12-7 7-7-7\" />\n\t\t</svg>\n\t);\n}\n\nexport interface MessageScrollerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, MessageScrollerButtonBaseProps {\n\t/** 自定义按钮内容 */\n\tchildren?: React.ReactNode;\n}\n\nconst MessageScrollerButton = React.forwardRef<HTMLButtonElement, MessageScrollerButtonProps>(({ className = '', direction = 'end', children, onClick, ...props }, ref) => {\n\tconst context = useMessageScrollerContext();\n\tconst styleKeys = getMessageScrollerButtonStyleKeys(direction);\n\n\tfunction handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n\t\tif (direction === 'end') {\n\t\t\tcontext.scrollToEnd();\n\t\t} else {\n\t\t\tcontext.scrollToStart();\n\t\t}\n\t\tonClick?.(event);\n\t}\n\n\treturn (\n\t\t<button\n\t\t\tref={ref}\n\t\t\ttype=\"button\"\n\t\t\tdata-slot=\"message-scroller-button\"\n\t\t\tdata-direction={direction}\n\t\t\tdata-active={context.buttonActive}\n\t\t\taria-label={direction === 'end' ? 'Scroll to end' : 'Scroll to start'}\n\t\t\tclassName={[styles[styleKeys.base], styles[styleKeys.direction], className].filter(Boolean).join(' ')}\n\t\t\tonClick={handleClick}\n\t\t\t{...props}>\n\t\t\t{children ?? <ArrowDownIcon />}\n\t\t</button>\n\t);\n});\nMessageScrollerButton.displayName = 'MessageScrollerButton';\n\nfunction useMessageScroller() {\n\treturn useMessageScrollerContext();\n}\n\nfunction useMessageScrollerScrollable() {\n\tconst context = useMessageScrollerContext();\n\treturn { scrollable: context.scrollable, isScrollable: context.scrollable };\n}\n\nfunction useMessageScrollerVisibility() {\n\tconst context = useMessageScrollerContext();\n\treturn { visible: context.buttonActive };\n}\n\nexport {\n\tMessageScrollerProvider,\n\tMessageScroller,\n\tMessageScrollerViewport,\n\tMessageScrollerContent,\n\tMessageScrollerItem,\n\tMessageScrollerButton,\n\tuseMessageScroller,\n\tuseMessageScrollerScrollable,\n\tuseMessageScrollerVisibility,\n};\nexport default MessageScroller;\n"
    },
    {
      "path": "packages/styles/scss/components/message-scroller.module.scss",
      "type": "registry:file",
      "target": "components/ui/message-scroller/message-scroller.module.scss",
      "content": "@use '../../../styles/variables/colors' as *;\n\n// 滚动容器根\n.root {\n\tposition: relative;\n\tdisplay: flex;\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 0;\n\tflex-direction: column;\n\toverflow: hidden;\n}\n\n// 视口\n.viewport {\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 0;\n\tmin-width: 0;\n\toverflow-y: auto;\n\toverscroll-behavior: contain;\n\tscrollbar-gutter: stable;\n\n\t&:focus-visible {\n\t\toutline: none;\n\t\tbox-shadow: var(--tile-message-scroller-focus-shadow, 0 0 0 3px color-mix(in srgb, $ring 50%, transparent));\n\t}\n\n\t&[data-autoscrolling='true'] {\n\t\tscrollbar-width: none;\n\n\t\t&::-webkit-scrollbar {\n\t\t\tdisplay: none;\n\t\t}\n\t}\n}\n\n// 内容\n.content {\n\tdisplay: flex;\n\theight: max-content;\n\tmin-height: 100%;\n\tflex-direction: column;\n\tgap: $spacing-8;\n}\n\n// 单个条目\n.item {\n\tmin-width: 0;\n\tflex-shrink: 0;\n\tcontent-visibility: auto;\n\tcontain-intrinsic-size: auto 10rem;\n\toverflow-anchor: none;\n\n\t&[data-scroll-anchor='true'] {\n\t\toverflow-anchor: auto;\n\t}\n}\n\n// 滚动按钮\n.button {\n\tposition: absolute;\n\tleft: 50%;\n\tz-index: 10;\n\tdisplay: inline-flex;\n\twidth: $spacing-8;\n\theight: $spacing-8;\n\talign-items: center;\n\tjustify-content: center;\n\tborder: 1px solid var(--tile-message-scroller-button-border, $border);\n\tborder-radius: $radius-full;\n\tbackground-color: var(--tile-message-scroller-button-background, $background);\n\tpadding: 0;\n\tcolor: $foreground;\n\tbox-shadow: var(--tile-message-scroller-button-shadow, $shadow-sm);\n\tcursor: pointer;\n\ttransition:\n\t\ttransform $transition-normal,\n\t\tscale $transition-normal,\n\t\topacity $transition-normal;\n\n\tsvg {\n\t\twidth: $spacing-4;\n\t\theight: $spacing-4;\n\t}\n\n\t&:hover {\n\t\tbackground-color: $muted;\n\t\tcolor: $foreground;\n\t}\n\n\t&.directionStart {\n\t\ttop: $spacing-4;\n\t\ttransform: translateX(-50%);\n\n\t\tsvg {\n\t\t\ttransform: rotate(180deg);\n\t\t}\n\t}\n\n\t&.directionEnd {\n\t\tbottom: $spacing-4;\n\t\ttransform: translateX(-50%);\n\t}\n\n\t&[data-active='false'] {\n\t\tpointer-events: none;\n\t\topacity: 0;\n\t\tscale: 0.95;\n\n\t\t&.directionEnd {\n\t\t\ttransform: translateX(-50%) translateY(100%);\n\t\t}\n\n\t\t&.directionStart {\n\t\t\ttransform: translateX(-50%) translateY(-100%);\n\t\t}\n\t}\n\n\t&[data-active='true'] {\n\t\topacity: 1;\n\t\tscale: 1;\n\t}\n}\n"
    },
    {
      "path": "packages/react/src/components/message-scroller/index.ts",
      "type": "registry:ui",
      "target": "components/ui/message-scroller/index.ts",
      "content": "export {\n\tMessageScrollerProvider,\n\tMessageScroller,\n\tMessageScrollerViewport,\n\tMessageScrollerContent,\n\tMessageScrollerItem,\n\tMessageScrollerButton,\n\tuseMessageScroller,\n\tuseMessageScrollerScrollable,\n\tuseMessageScrollerVisibility,\n\ttype MessageScrollerProviderProps,\n\ttype MessageScrollerProps,\n\ttype MessageScrollerViewportProps,\n\ttype MessageScrollerContentProps,\n\ttype MessageScrollerItemProps,\n\ttype MessageScrollerButtonProps,\n} from './message-scroller';\n"
    }
  ]
}
