{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "overlay-scroll",
  "title": "Overlay Scroll",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/overlay-scroll/overlay-scroll.component.ts",
      "content": "import {\n  Component,\n  Input,\n  ViewChild,\n  ElementRef,\n  AfterViewInit,\n  OnDestroy,\n  booleanAttribute,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\nexport interface OverlayScrollMetrics {\n  scrollHeight: number\n  clientHeight: number\n  scrollTop: number\n}\n\nconst overlayScrollCss = `\n.overlay-scroll__inner {\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n  overscroll-behavior: contain;\n}\n.overlay-scroll__inner::-webkit-scrollbar {\n  display: none;\n  width: 0;\n  height: 0;\n}\n.overlay-scroll__thumb {\n  position: absolute;\n  top: 0;\n  border-radius: 2px;\n  background: var(--muted-foreground);\n  opacity: 0;\n  pointer-events: none;\n  touch-action: none;\n  user-select: none;\n  -webkit-user-select: none;\n  transition:\n    opacity 0.2s ease,\n    background-color 0.15s,\n    width 0.12s ease;\n  will-change: transform, opacity;\n}\n.overlay-scroll__thumb--draggable {\n  cursor: pointer;\n}\n.overlay-scroll__thumb--visible {\n  opacity: 0.4;\n  pointer-events: auto;\n}\n:host:hover .overlay-scroll__thumb--visible,\n.overlay-scroll:hover .overlay-scroll__thumb--visible {\n  opacity: 0.6;\n}\n:host:hover .overlay-scroll__thumb--draggable:hover,\n.overlay-scroll:hover .overlay-scroll__thumb--draggable:hover {\n  opacity: 0.8;\n  width: 8px !important;\n  background: var(--foreground);\n}\n.overlay-scroll__thumb--dragging {\n  opacity: 1 !important;\n  background: var(--foreground) !important;\n  width: 8px !important;\n}\n`\n\n/**\n * Angular port of UIPKGE OverlayScroll. Slack-style overlay scrollbar: the\n * native scrollbar is hidden so content uses the full width, then a thin\n * auto-fading thumb is drawn on top (drag-to-scroll via Pointer Events).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-overlay-scroll, [ui-overlay-scroll]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"overlay-scroll\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n    '(mouseenter)': 'onEnter()',\n    '(mouseleave)': 'onLeave()',\n  },\n  template: `\n    <style>\n      ${overlayScrollCss}\n    </style>\n    <div #scrollerEl data-uipkge data-slot=\"overlay-scroll-viewport\" [class]=\"innerClass\" (scroll)=\"onScroll()\">\n      <ng-content />\n    </div>\n    @if (thumbHeight > 0 && showThumb) {\n      <div\n        #thumbEl\n        data-uipkge\n        data-slot=\"overlay-scroll-thumb\"\n        aria-hidden=\"true\"\n        [class]=\"thumbClass\"\n        [style]=\"thumbStyle\"\n        (pointerdown)=\"onThumbPointerDown($event)\"\n      ></div>\n    }\n  `,\n})\nexport class UiOverlayScrollComponent implements AfterViewInit, OnDestroy {\n  @Input() thumbWidth = 4\n  @Input() thumbOffset = 2\n  @Input() idleHideMs = 800\n  @Input({ transform: booleanAttribute }) draggable = true\n  @Input('class') className?: string\n\n  @ViewChild('scrollerEl', { static: false }) scrollerRef?: ElementRef<HTMLElement>\n  @ViewChild('thumbEl', { static: false }) thumbRef?: ElementRef<HTMLElement>\n\n  thumbHeight = 0\n  thumbTop = 0\n  showThumb = false\n  isHovered = false\n  isDragging = false\n  private hideTimer: ReturnType<typeof setTimeout> | null = null\n  private resizeObserver: ResizeObserver | null = null\n  private mutationObserver: MutationObserver | null = null\n\n  private activePointerId: number | null = null\n  private dragStartY = 0\n  private dragStartScrollTop = 0\n\n  get scrollerEl(): HTMLElement | null {\n    return this.scrollerRef?.nativeElement ?? null\n  }\n\n  get hostClass(): string {\n    return cn('block overlay-scroll relative', this.className)\n  }\n\n  get innerClass(): string {\n    return cn('overlay-scroll__inner h-full overflow-x-hidden overflow-y-auto')\n  }\n\n  get thumbClass(): string {\n    return cn(\n      'overlay-scroll__thumb',\n      this.showThumb && 'overlay-scroll__thumb--visible',\n      this.isDragging && 'overlay-scroll__thumb--dragging',\n      this.draggable && 'overlay-scroll__thumb--draggable',\n    )\n  }\n\n  get thumbStyle(): Record<string, string> {\n    const width = this.isHovered || this.isDragging ? this.thumbWidth * 2 : this.thumbWidth\n    return {\n      height: `${this.thumbHeight}px`,\n      transform: `translateY(${this.thumbTop}px)`,\n      width: `${width}px`,\n      right: `var(--ovs-thumb-right, ${this.thumbOffset}px)`,\n    }\n  }\n\n  ngAfterViewInit(): void {\n    this.recompute()\n    const el = this.scrollerEl\n    if (!el) return\n\n    if (typeof ResizeObserver !== 'undefined') {\n      this.resizeObserver = new ResizeObserver(() => this.recompute())\n      this.resizeObserver.observe(el)\n    }\n\n    const inner = el.firstElementChild as HTMLElement | null\n    if (inner) {\n      if (typeof ResizeObserver !== 'undefined') {\n        this.resizeObserver?.observe(inner)\n      }\n      if (typeof MutationObserver !== 'undefined') {\n        this.mutationObserver = new MutationObserver(() => this.recompute())\n        this.mutationObserver.observe(inner, { childList: true, subtree: true })\n      }\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.resizeObserver?.disconnect()\n    this.mutationObserver?.disconnect()\n    if (this.hideTimer) clearTimeout(this.hideTimer)\n    this.endDrag()\n  }\n\n  recompute(m?: OverlayScrollMetrics): void {\n    const el = this.scrollerEl\n    const clientHeight = m ? m.clientHeight : (el?.clientHeight ?? 0)\n    const scrollHeight = m ? m.scrollHeight : (el?.scrollHeight ?? 0)\n    const scrollTop = m ? m.scrollTop : (el?.scrollTop ?? 0)\n\n    const ratio = clientHeight / scrollHeight\n    if (!Number.isFinite(ratio) || ratio >= 1) {\n      this.thumbHeight = 0\n      return\n    }\n    this.thumbHeight = Math.max(24, clientHeight * ratio)\n    const maxScroll = scrollHeight - clientHeight\n    const maxThumb = clientHeight - this.thumbHeight\n    this.thumbTop = maxScroll > 0 ? (scrollTop / maxScroll) * maxThumb : 0\n  }\n\n  flashThumb(): void {\n    if (this.thumbHeight === 0) return\n    this.showThumb = true\n    if (this.hideTimer) clearTimeout(this.hideTimer)\n    this.hideTimer = setTimeout(() => {\n      if (!this.isHovered && !this.isDragging) this.showThumb = false\n    }, this.idleHideMs)\n  }\n\n  hideThumb(): void {\n    if (this.hideTimer) clearTimeout(this.hideTimer)\n    this.hideTimer = null\n    if (!this.isDragging) this.showThumb = false\n  }\n\n  onScroll(m?: OverlayScrollMetrics): void {\n    this.recompute(m)\n    this.flashThumb()\n  }\n\n  onEnter(): void {\n    this.isHovered = true\n    this.recompute()\n    if (this.thumbHeight > 0) this.showThumb = true\n  }\n\n  onLeave(): void {\n    this.isHovered = false\n    if (this.isDragging) return\n    if (this.hideTimer) clearTimeout(this.hideTimer)\n    this.showThumb = false\n  }\n\n  private onPointerMove = (e: PointerEvent) => {\n    if (this.activePointerId !== e.pointerId) return\n    const el = this.scrollerEl\n    if (!el) return\n    const maxScroll = el.scrollHeight - el.clientHeight\n    const maxThumb = el.clientHeight - this.thumbHeight\n    if (maxThumb <= 0) return\n    const scrollRatio = maxScroll / maxThumb\n    el.scrollTop = this.dragStartScrollTop + (e.clientY - this.dragStartY) * scrollRatio\n  }\n\n  private endDrag = (e?: PointerEvent) => {\n    if (e && this.activePointerId !== e.pointerId) return\n    this.isDragging = false\n    const thumb = this.thumbRef?.nativeElement\n    if (thumb && this.activePointerId !== null) {\n      try {\n        thumb.releasePointerCapture(this.activePointerId)\n      } catch {}\n    }\n    this.activePointerId = null\n    thumb?.removeEventListener('pointermove', this.onPointerMove)\n    thumb?.removeEventListener('pointerup', this.endDrag)\n    thumb?.removeEventListener('pointercancel', this.endDrag)\n    if (!this.isHovered) this.showThumb = false\n  }\n\n  onThumbPointerDown(e: PointerEvent): void {\n    if (!this.draggable || !this.scrollerEl || !this.thumbRef?.nativeElement) return\n    if (e.pointerType === 'mouse' && e.button !== 0) return\n    e.preventDefault()\n    this.isDragging = true\n    this.activePointerId = e.pointerId\n    this.dragStartY = e.clientY\n    this.dragStartScrollTop = this.scrollerEl.scrollTop\n    const thumb = this.thumbRef.nativeElement\n    thumb.setPointerCapture(e.pointerId)\n    thumb.addEventListener('pointermove', this.onPointerMove)\n    thumb.addEventListener('pointerup', this.endDrag)\n    thumb.addEventListener('pointercancel', this.endDrag)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/overlay-scroll/overlay-scroll.component.ts"
    },
    {
      "path": "packages/registry-angular/components/overlay-scroll/index.ts",
      "content": "export { UiOverlayScrollComponent, type OverlayScrollMetrics } from './overlay-scroll.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/overlay-scroll/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Slack-style overlay scrollbar. Hides the native scrollbar so the scrolled content uses the full container width (no gutter reservation), then draws a thin auto-fading thumb absolutely positioned on top. Drag-to-scroll via Pointer Events covers mouse, touch, and pen. Vertical only. The component does not enforce a height — give it a bounded height via the parent (`flex-1 min-h-0` in a flex column, or a fixed `h-*` / `max-h-*`).",
  "categories": [
    "layout"
  ]
}