{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-list",
  "title": "MessageList",
  "description": "A scrolling column that follows new messages until you scroll up.",
  "registryDependencies": [
    "https://creance.pages.dev/r/button.json",
    "https://creance.pages.dev/r/creance-base.json"
  ],
  "files": [
    {
      "path": "message-list/MessageList.css",
      "content": ".cr-message-list {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n  height: 100%;\n}\n\n.cr-message-list__scroll {\n  flex: 1;\n  min-height: 0;\n  overflow-y: auto;\n  overscroll-behavior: contain;\n}\n\n.cr-message-list__scroll:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: calc(-1 * var(--cr-focus-width));\n}\n\n.cr-message-list__inner {\n  display: flex;\n  flex-direction: column;\n  gap: var(--cr-space-6);\n  box-sizing: border-box;\n  width: 100%;\n  max-width: var(--cr-message-list-width, 760px);\n  margin-inline: auto;\n  padding: var(--cr-space-6) var(--cr-space-4);\n}\n\n.cr-message-list__jump {\n  position: absolute;\n  z-index: 1;\n  bottom: var(--cr-space-3);\n  left: 50%;\n  transform: translateX(-50%);\n  box-shadow: var(--cr-shadow-md);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/message-list/MessageList.css"
    },
    {
      "path": "message-list/MessageList.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button } from '@/components/creance/button/Button';\nimport { cx } from '@/lib/creance/cx';\nimport { ArrowDownIcon } from '@/lib/creance/icons';\nimport { LiveRegion } from '@/lib/creance/LiveRegion';\nimport './MessageList.css';\n\nconst PIN_THRESHOLD = 24;\n\nexport interface MessageListProps {\n  /** The messages, oldest first. */\n  children: ReactNode;\n  /** Accessible name of the scrollable region. Default \"Conversation\". */\n  label?: string;\n  /** Text of the button shown when new content arrives while the person has scrolled up. Default \"Jump to latest\". */\n  jumpLabel?: string;\n  /**\n   * Read out politely when it changes, for example \"Deploy Fixer replied\". Streaming text is not announced\n   * word by word; say what happened once it has.\n   */\n  announcement?: string;\n  className?: string;\n}\n\n/**\n * A scrolling column of messages that follows new content. Following pauses when the person scrolls up,\n * and a button brings them back.\n */\nexport function MessageList({ children, label = 'Conversation', jumpLabel = 'Jump to latest', announcement = '', className }: MessageListProps) {\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const innerRef = useRef<HTMLDivElement>(null);\n  const pinned = useRef(true);\n  const jumping = useRef(false);\n  const [hasNew, setHasNew] = useState(false);\n\n  // The content grows as messages arrive and as text streams in: follow it, or offer the way back.\n  useEffect(() => {\n    const el = scrollRef.current;\n    const inner = innerRef.current;\n    if (!el || !inner) return;\n    const follow = () => {\n      if (pinned.current) el.scrollTop = el.scrollHeight;\n      else if (el.scrollHeight > el.clientHeight) setHasNew(true);\n    };\n    follow();\n    const observer = new ResizeObserver(follow);\n    observer.observe(inner);\n    return () => observer.disconnect();\n  }, []);\n\n  function onScroll() {\n    const el = scrollRef.current;\n    if (!el) return;\n    const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < PIN_THRESHOLD;\n    if (jumping.current) {\n      if (atBottom) jumping.current = false;\n      return;\n    }\n    pinned.current = atBottom;\n    if (atBottom) setHasNew(false);\n  }\n\n  function jumpToLatest() {\n    const el = scrollRef.current;\n    if (!el) return;\n    const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n    pinned.current = true;\n    jumping.current = !reduce;\n    setHasNew(false);\n    el.scrollTo({ top: el.scrollHeight, behavior: reduce ? 'auto' : 'smooth' });\n  }\n\n  return (\n    <div className={cx('cr-message-list', className)}>\n      {/* Focusable so keyboard users can scroll it. */}\n      {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}\n      <div ref={scrollRef} className=\"cr-message-list__scroll\" role=\"region\" aria-label={label} tabIndex={0} onScroll={onScroll}>\n        <div ref={innerRef} className=\"cr-message-list__inner\">\n          {children}\n        </div>\n      </div>\n      {hasNew && (\n        <Button size=\"sm\" className=\"cr-message-list__jump\" onClick={jumpToLatest}>\n          <ArrowDownIcon /> {jumpLabel}\n        </Button>\n      )}\n      <LiveRegion message={announcement} />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/message-list/MessageList.tsx"
    }
  ],
  "categories": [
    "molecule",
    "chat"
  ],
  "type": "registry:ui"
}