{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "scroll-progress",
  "title": "Scroll Progress",
  "type": "registry:ui",
  "description": "Slim reading-progress bar pinned to the top of the viewport or of any supplied scrollable container. Scales horizontally with scroll depth; supports fixed and absolute (contained) modes, lerp smoothing with a prefers-reduced-motion fallback, custom thickness, and any CSS color or gradient.",
  "categories": [
    "feedback",
    "utility"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/scroll-progress/scroll-progress.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\n// Boolean props whose React default is `true`: `attr=\"false\"` turns them off.\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\n/**\n * <uip-scroll-progress> — the registry ScrollProgress as a web component.\n *\n * Class string is React's verbatim, on the same single element (the bar\n * itself, scaled with `scaleX(progress)`). The host is `display: contents` —\n * the bar is `fixed` (viewport) or `absolute` (contained) like React's.\n *\n * `container` is the scrollable element to measure (property only — an\n * HTMLElement, like React; defaults to the window/document). `smooth`\n * lerp-eases the bar toward its target each frame (factor 0.18, settling\n * within a thousandth); under `prefers-reduced-motion` it tracks raw scroll\n * regardless of the prop, like React.\n *\n * Multiple fixed instances on one page stack into lanes — offset each with a\n * host class (e.g. `class=\"top-2\"`), exactly like the React demo.\n */\nexport class UipScrollProgress extends LitElement {\n  // The bar positions itself (fixed/absolute); the host adds no box.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    height: { type: Number },\n    color: {},\n    position: { reflect: true },\n    container: { attribute: false },\n    smooth: { converter: trueByDefault },\n    progress: { state: true },\n  }\n\n  height = 3\n  color = 'var(--primary)'\n  position: 'fixed' | 'absolute' = 'fixed'\n  container?: HTMLElement | null\n  smooth = true\n  private progress = 0\n\n  private display = 0\n  private target = 0\n  private raf: number | null = null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'scroll-progress')\n    this.attach()\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.detach()\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    // Swap container at runtime: re-attach listeners to the new element.\n    if (changed.has('container')) {\n      this.detach()\n      this.attach()\n    }\n  }\n\n  private get bound(): EventTarget | null {\n    if (typeof window === 'undefined') return null\n    return this.container ?? window\n  }\n\n  private read() {\n    const clamp01 = (v: number) => Math.min(1, Math.max(0, v))\n    if (this.container) {\n      const max = this.container.scrollHeight - this.container.clientHeight\n      return max > 0 ? clamp01(this.container.scrollTop / max) : 0\n    }\n    const doc = document.documentElement\n    const scrollTop = window.scrollY || doc.scrollTop || document.body.scrollTop || 0\n    const max = doc.scrollHeight - doc.clientHeight\n    return max > 0 ? clamp01(scrollTop / max) : 0\n  }\n\n  private stopLoop() {\n    if (this.raf !== null) {\n      cancelAnimationFrame(this.raf)\n      this.raf = null\n    }\n  }\n\n  private tick = () => {\n    this.display += (this.target - this.display) * 0.18\n    this.progress = this.display\n    if (Math.abs(this.target - this.display) < 0.001) {\n      this.display = this.target\n      this.progress = this.target\n      this.raf = null\n      return\n    }\n    this.raf = requestAnimationFrame(this.tick)\n  }\n\n  private sync = () => {\n    this.target = this.read()\n    const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    // Read `smooth` through the field so toggling it does not re-attach\n    // listeners (mirrors React's smoothRef).\n    if (!this.smooth || reducedMotion) {\n      this.stopLoop()\n      this.display = this.target\n      this.progress = this.target\n      return\n    }\n    if (this.raf === null) this.raf = requestAnimationFrame(this.tick)\n  }\n\n  private attach() {\n    const bound = this.bound\n    if (!bound) return\n    bound.addEventListener('scroll', this.sync, { passive: true })\n    window.addEventListener('resize', this.sync)\n    // Sync instantly on mount / container swap so the bar never animates up\n    // from zero.\n    this.target = this.read()\n    this.display = this.target\n    this.progress = this.target\n  }\n\n  private detach() {\n    this.bound?.removeEventListener('scroll', this.sync)\n    if (typeof window !== 'undefined') window.removeEventListener('resize', this.sync)\n    this.stopLoop()\n  }\n\n  render() {\n    return html`<div\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"scroll-progress\"\n      aria-hidden=\"true\"\n      data-position=${this.position}\n      data-smooth=${this.smooth ? '' : nothing}\n      class=${cn('pointer-events-none top-0 left-0 z-50 w-full origin-left', this.position === 'fixed' ? 'fixed' : 'absolute')}\n      style=${styleMap({\n        height: `${this.height}px`,\n        background: this.color,\n        transform: `scaleX(${this.progress})`,\n        willChange: 'transform',\n      })}\n    ></div>`\n  }\n}\n\ncustomElements.get('uip-scroll-progress') || customElements.define('uip-scroll-progress', UipScrollProgress)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-scroll-progress': UipScrollProgress\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/scroll-progress/scroll-progress.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
