{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "scroll-progress",
  "title": "Scroll Progress",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/scroll-progress/scroll-progress.component.ts",
      "content": "import {\n  Component,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  SimpleChanges,\n  booleanAttribute,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\nexport type ScrollProgressPosition = 'fixed' | 'absolute'\n\n/**\n * Angular port of UIPKGE ScrollProgress (React `ScrollProgress`). A single aria-hidden bar\n * scaled horizontally by scroll depth of the window (default) or of `container`. Listens to\n * scroll + resize, syncs instantly on mount / container swap, lerps toward the target each\n * frame (0.18) when `smooth`, and tracks 1:1 under prefers-reduced-motion. Progress is a\n * signal: scroll handlers and animation frames repaint in zoneless apps.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-scroll-progress, [ui-scroll-progress]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"scroll-progress\"',\n    '[attr.aria-hidden]': '\"true\"',\n    '[attr.data-position]': 'position',\n    '[attr.data-smooth]': 'smooth ? \"true\" : null',\n    '[class]': 'hostClass',\n    '[style.height.px]': 'height',\n    '[style.background]': 'color',\n    '[style.transform]': '\"scaleX(\" + progress() + \")\"',\n    '[style.will-change]': '\"transform\"',\n  },\n  template: ``,\n})\nexport class UiScrollProgressComponent implements OnInit, OnChanges, OnDestroy {\n  /** Bar thickness in pixels. */\n  @Input() height = 3\n  /** Any CSS color or gradient — applied to `background` verbatim. */\n  @Input() color = 'var(--primary)'\n  /** `fixed` pins to the viewport; `absolute` fills a positioned scrollable parent. */\n  @Input() position: ScrollProgressPosition = 'fixed'\n  /** Scrollable element to measure. Defaults to the window/document. */\n  @Input() container: HTMLElement | null = null\n  /** Lerp-smooth the displayed value toward the real progress each frame. */\n  @Input({ transform: booleanAttribute }) smooth = true\n  @Input('class') className?: string\n\n  readonly progress = signal(0)\n  private display = 0\n  private target = 0\n  private raf: number | null = null\n  private detach: (() => void) | null = null\n  private attached = false\n\n  get hostClass(): string {\n    return cn(\n      'block pointer-events-none top-0 left-0 z-50 w-full origin-left',\n      this.position === 'fixed' ? 'fixed' : 'absolute',\n      this.className,\n    )\n  }\n\n  ngOnInit(): void {\n    this.attach()\n  }\n\n  /** Swapping `container` re-attaches the listeners to the new element. */\n  ngOnChanges(changes: SimpleChanges): void {\n    if (this.attached && changes['container']) this.attach()\n  }\n\n  ngOnDestroy(): void {\n    this.detach?.()\n    this.detach = null\n  }\n\n  private read(): number {\n    const clamp01 = (value: number) => Math.min(1, Math.max(0, value))\n    const el = this.container\n    if (el) {\n      const max = el.scrollHeight - el.clientHeight\n      return max > 0 ? clamp01(el.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(): void {\n    if (this.raf !== null) {\n      cancelAnimationFrame(this.raf)\n      this.raf = null\n    }\n  }\n\n  private readonly tick = (): void => {\n    this.display += (this.target - this.display) * 0.18\n    this.progress.set(this.display)\n    if (Math.abs(this.target - this.display) < 0.001) {\n      this.display = this.target\n      this.progress.set(this.target)\n      this.raf = null\n      return\n    }\n    this.raf = requestAnimationFrame(this.tick)\n  }\n\n  private readonly sync = (): void => {\n    this.target = this.read()\n    const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false\n    if (!this.smooth || reducedMotion) {\n      this.stopLoop()\n      this.display = this.target\n      this.progress.set(this.target)\n      return\n    }\n    if (this.raf === null) this.raf = requestAnimationFrame(this.tick)\n  }\n\n  private attach(): void {\n    this.attached = true\n    this.detach?.()\n    this.detach = null\n    if (typeof window === 'undefined') return\n    // EventTarget union keeps addEventListener compatible across HTMLElement | Window.\n    const bound: EventTarget = this.container ?? window\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 from zero.\n    this.target = this.read()\n    this.display = this.target\n    this.progress.set(this.target)\n    this.detach = () => {\n      bound.removeEventListener('scroll', this.sync)\n      window.removeEventListener('resize', this.sync)\n      this.stopLoop()\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/scroll-progress/scroll-progress.component.ts"
    },
    {
      "path": "packages/registry-angular/components/scroll-progress/index.ts",
      "content": "export { UiScrollProgressComponent, type ScrollProgressPosition } from './scroll-progress.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/scroll-progress/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "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"
  ]
}