{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-scroller",
  "title": "Message Scroller",
  "description": "Scrolls a conversation, sticking to the newest message, on shadcn's message-scroller.",
  "dependencies": [
    "@hugeicons/core-free-icons",
    "@hugeicons/react",
    "@shadcn/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json",
    "https://constella.aayush.cv/r/button.json"
  ],
  "files": [
    {
      "path": "src/components/ai/message-scroller.tsx",
      "content": "\"use client\";\n\nimport { ArrowDown02Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { MessageScroller as MessageScrollerPrimitive } from \"@shadcn/react/message-scroller\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport type { ComponentProps } from \"react\";\n\nimport { Button } from \"@/components/primitives/button\";\nimport type { Sx } from \"@/lib/constella/styles\";\nimport {\n  durations,\n  easings,\n  media,\n  motion,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\ntype Styled<T> = Omit<T, \"className\" | \"style\"> & {\n  sx?: Sx;\n};\n\nconst away = \":is([data-active='false'])\";\n\nconst styles = stylex.create({\n  root: {\n    display: \"flex\",\n    flexDirection: \"column\",\n    height: \"100%\",\n    minHeight: 0,\n    overflow: \"hidden\",\n    position: \"relative\",\n    width: \"100%\",\n  },\n  viewport: {\n    contain: \"content\",\n    height: \"100%\",\n    minHeight: 0,\n    minWidth: 0,\n    overflowY: \"auto\",\n    overscrollBehavior: \"contain\",\n    scrollbarGutter: \"stable\",\n    scrollbarWidth: \"thin\",\n    // Hidden until the opening position is applied, so the first frame never shows the top.\n    visibility: { default: \"visible\", \":is([data-pending-scroll])\": \"hidden\" },\n    width: \"100%\",\n  },\n  content: {\n    display: \"flex\",\n    flexDirection: \"column\",\n    height: \"max-content\",\n    minHeight: \"100%\",\n  },\n  item: {\n    flexShrink: 0,\n    minWidth: 0,\n  },\n  // Rises in while there's more below, and drops away at the end.\n  button: {\n    insetBlockEnd: space.md,\n    insetInlineEnd: 0,\n    insetInlineStart: 0,\n    marginInlineEnd: \"auto\",\n    marginInlineStart: \"auto\",\n    opacity: { default: 1, [away]: 0 },\n    pointerEvents: { default: \"auto\", [away]: \"none\" },\n    position: \"absolute\",\n    transform: {\n      default: \"none\",\n      [away]: {\n        default: `translateY(${motion.exitOffset}) scale(${motion.popoverScale})`,\n        [media.reducedMotion]: \"none\",\n      },\n    },\n    transitionDuration: durations.popover,\n    transitionProperty: \"opacity, transform\",\n    transitionTimingFunction: easings.out,\n  },\n});\n\n/** Holds the scroll state: sticks to the end as messages arrive, and keeps its place when history loads above. */\nconst MessageScrollerProvider = MessageScrollerPrimitive.Provider;\n\nfunction MessageScroller({\n  sx,\n  ...props\n}: Styled<ComponentProps<typeof MessageScrollerPrimitive.Root>>) {\n  return (\n    <MessageScrollerPrimitive.Root\n      data-slot=\"message-scroller\"\n      {...props}\n      {...stylex.props(styles.root, sx)}\n    />\n  );\n}\n\nfunction MessageScrollerViewport({\n  sx,\n  ...props\n}: Styled<ComponentProps<typeof MessageScrollerPrimitive.Viewport>>) {\n  return (\n    <MessageScrollerPrimitive.Viewport\n      data-slot=\"message-scroller-viewport\"\n      {...props}\n      {...stylex.props(styles.viewport, sx)}\n    />\n  );\n}\n\nfunction MessageScrollerContent({\n  sx,\n  ...props\n}: Styled<ComponentProps<typeof MessageScrollerPrimitive.Content>>) {\n  return (\n    <MessageScrollerPrimitive.Content\n      data-slot=\"message-scroller-content\"\n      {...props}\n      {...stylex.props(styles.content, sx)}\n    />\n  );\n}\n\nfunction MessageScrollerItem({\n  scrollAnchor = false,\n  sx,\n  ...props\n}: Styled<ComponentProps<typeof MessageScrollerPrimitive.Item>>) {\n  return (\n    <MessageScrollerPrimitive.Item\n      data-slot=\"message-scroller-item\"\n      scrollAnchor={scrollAnchor}\n      {...props}\n      {...stylex.props(styles.item, sx)}\n    />\n  );\n}\n\n/** Scrolls to the newest message; shown only while there's more below. */\nfunction MessageScrollerButton({\n  sx,\n  ...props\n}: Styled<\n  Omit<\n    ComponentProps<typeof MessageScrollerPrimitive.Button>,\n    \"children\" | \"direction\" | \"render\"\n  >\n>) {\n  return (\n    <MessageScrollerPrimitive.Button\n      data-slot=\"message-scroller-button\"\n      {...props}\n      render={\n        <Button\n          aria-label=\"Scroll to end\"\n          size=\"icon-sm\"\n          sx={[styles.button, sx]}\n          variant=\"outline\"\n        />\n      }\n    >\n      <HugeiconsIcon\n        aria-hidden\n        icon={ArrowDown02Icon}\n        size={sizes.icon}\n        strokeWidth={Number(strokes.icon)}\n      />\n    </MessageScrollerPrimitive.Button>\n  );\n}\n\nexport {\n  MessageScroller,\n  MessageScrollerButton,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerProvider,\n  MessageScrollerViewport,\n};\nexport {\n  useMessageScroller,\n  useMessageScrollerScrollable,\n  useMessageScrollerVisibility,\n} from \"@shadcn/react/message-scroller\";\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/message-scroller/index.md",
  "type": "registry:ui"
}