{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-scroll-anchor",
  "type": "registry:component",
  "title": "Scroll anchor",
  "description": "Streaming never steals your scroll position; a pill offers the way back down.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/scroll-anchor.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentProps,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { ArrowDownIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { field, floating, paper } from \"./surfaces\";\n\nexport interface ScrollAnchorMessage {\n  role: \"user\" | \"assistant\";\n  text: string;\n}\n\nconst INITIAL_COUNT = 3;\nconst APPEND_MS = 1300;\n\nexport function ScrollAnchor({\n  messages,\n  paused = false,\n  onSettled,\n  className,\n  ...props\n}: Omit<\n  ComponentProps<\"div\">,\n  \"children\" | \"messages\" | \"paused\" | \"onSettled\"\n> & {\n  messages: ScrollAnchorMessage[];\n  paused?: boolean;\n  onSettled?: () => void;\n}) {\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const [count, setCount] = useState(INITIAL_COUNT);\n  const [pinned, setPinned] = useState(true);\n  const [seenCount, setSeenCount] = useState(INITIAL_COUNT);\n\n  useEffect(() => {\n    if (paused) return;\n    const id = setInterval(() => {\n      setCount((current) => {\n        if (current >= messages.length) return current;\n        return current + 1;\n      });\n    }, APPEND_MS);\n    return () => clearInterval(id);\n  }, [messages.length, paused]);\n\n  useEffect(() => {\n    if (count >= messages.length && pinned) onSettled?.();\n  }, [count, pinned, messages.length, onSettled]);\n\n  useEffect(() => {\n    if (count === INITIAL_COUNT + 1) {\n      setPinned(false);\n      const viewport = viewportRef.current;\n      if (viewport) viewport.scrollTo({ top: 0, behavior: \"smooth\" });\n    }\n  }, [count]);\n\n  useEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport) return;\n    if (pinned) {\n      viewport.scrollTo({ top: viewport.scrollHeight });\n      setSeenCount(count);\n    }\n  }, [count, pinned]);\n\n  const jump = useCallback(() => {\n    const viewport = viewportRef.current;\n    if (viewport) {\n      viewport.scrollTo({ top: viewport.scrollHeight, behavior: \"smooth\" });\n    }\n    setPinned(true);\n    setSeenCount(count);\n  }, [count]);\n\n  useEffect(() => {\n    if (paused || pinned || count - seenCount < 2) return;\n    const id = setTimeout(jump, 2400);\n    return () => clearTimeout(id);\n  }, [paused, pinned, count, seenCount, jump]);\n\n  const newCount = count - seenCount;\n\n  return (\n    <div\n      data-slot=\"scroll-anchor\"\n      className={cn(\n        paper,\n        \"relative h-64 w-full max-w-sm overflow-hidden rounded-2xl\",\n        className,\n      )}\n\n      {...props}\n    >\n      <div\n        ref={viewportRef}\n        className=\"flex h-full flex-col gap-2.5 overflow-y-hidden scroll-smooth p-4\"\n      >\n        {messages.slice(0, count).map((message, i) => (\n          <div\n            key={i}\n            className={cn(\n              \"fade-in slide-in-from-bottom-1 animate-in max-w-[85%] text-xs leading-relaxed duration-300 motion-reduce:animate-none\",\n              message.role === \"user\"\n                ? cn(field, \"self-end rounded-2xl px-3 py-1.5\")\n                : \"text-foreground/55 self-start\",\n            )}\n          >\n            {message.text}\n          </div>\n        ))}\n      </div>\n      <div\n        aria-hidden\n        className=\"from-background dark:from-popover pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b to-transparent\"\n      />\n      {!pinned && newCount > 0 && (\n        <button\n          type=\"button\"\n          onClick={jump}\n          className={cn(\n            floating,\n            \"fade-in slide-in-from-bottom-2 animate-in absolute inset-x-0 bottom-3 mx-auto flex w-fit items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs transition-transform duration-200 hover:-translate-y-px\",\n          )}\n        >\n          <ArrowDownIcon className=\"size-3 opacity-60\" />\n          {newCount} new {newCount === 1 ? \"message\" : \"messages\"}\n        </button>\n      )}\n    </div>\n  );\n}\n"
    }
  ]
}