{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "loading-bar",
  "title": "Loading Bar",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/loading-bar/loading-bar.component.ts",
      "content": "import {\n  Component,\n  DestroyRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  Output,\n  SimpleChanges,\n  ViewEncapsulation,\n  booleanAttribute,\n  inject,\n  signal,\n  type Signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\n\n// Copied verbatim from the React loading-bar (injected there as an inline <style>). Unscoped\n// (ViewEncapsulation.None) so the class matches the template's indeterminate bar.\nconst LOADING_BAR_MOTION_STYLES = `\n@media (prefers-reduced-motion: no-preference) {\n  .loading-bar-indeterminate {\n    animation: loading-bar-slide 1.2s ease-in-out infinite;\n  }\n}\n@keyframes loading-bar-slide {\n  0% { left: -33%; }\n  100% { left: 100%; }\n}\n`\n\n/** Imperative API — same shape as React's `LoadingBarHandle`. */\nexport interface LoadingBarHandle {\n  start: (from?: number) => void\n  finish: () => void\n  error: () => void\n  fail: () => void\n  inc: (amount?: number) => void\n  set: (value: number) => void\n}\n\n/**\n * Angular port of UIPKGE LoadingBar (React `LoadingBar`). NProgress-style bar fixed to the\n * top / bottom of the viewport. Drive it with `value` / `valueChange`, or imperatively\n * (`start` trickles toward 95 on animation frames, `finish` fills to 100 then fades and\n * resets, `error` / `fail` tints destructive then fades). State is signal-backed so timers\n * and frames repaint in zoneless apps.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-loading-bar, [ui-loading-bar]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  styles: [LOADING_BAR_MOTION_STYLES],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"loading-bar\"',\n    '[attr.data-position]': 'position',\n    '[attr.data-state]': 'isError ? \"error\" : indeterminate ? \"indeterminate\" : \"determinate\"',\n    '[class]': 'hostClass',\n    '[style.height.px]': 'height',\n    '[attr.role]': '\"progressbar\"',\n    '[attr.aria-valuemin]': '0',\n    '[attr.aria-valuemax]': '100',\n    '[attr.aria-valuenow]': 'indeterminate ? null : pct',\n    '[attr.aria-busy]': 'visible && !isError ? \"true\" : null',\n    '[attr.aria-hidden]': 'visible ? \"false\" : \"true\"',\n    // `hidden` is an input (React prop), never the native attribute on the host.\n    '[attr.hidden]': 'null',\n  },\n  template: `\n    <div class=\"absolute inset-0 bg-transparent\"></div>\n    @if (indeterminate) {\n      <div\n        data-slot=\"loading-bar-indeterminate\"\n        class=\"loading-bar-indeterminate absolute inset-y-0 w-1/3\"\n        [style.background-color]=\"barColor\"\n      >\n        @if (spinner) {\n          <div\n            data-slot=\"loading-bar-spinner\"\n            class=\"absolute top-1/2 right-0 size-3 translate-x-1/2 -translate-y-1/2 animate-spin rounded-full border-2 border-current border-t-transparent\"\n            [style.color]=\"barColor\"\n          ></div>\n        }\n      </div>\n    } @else {\n      <div\n        data-slot=\"loading-bar-fill\"\n        class=\"absolute inset-y-0 left-0 transition-[width] duration-200 ease-out\"\n        [style.width.%]=\"pct\"\n        [style.background-color]=\"barColor\"\n      >\n        @if (spinner) {\n          <div\n            data-slot=\"loading-bar-spinner\"\n            class=\"absolute top-1/2 right-0 size-3 translate-x-1/2 -translate-y-1/2 animate-spin rounded-full border-2 border-current border-t-transparent\"\n            [style.color]=\"barColor\"\n          ></div>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiLoadingBarComponent implements OnChanges, LoadingBarHandle {\n  /** 0–100 progress value. Use with value / valueChange or drive it imperatively. */\n  @Input() value = 0\n  /** Bar color. Accepts any CSS color value. */\n  @Input() color = ''\n  /** Bar height in px. */\n  @Input() height = 3\n  /** Indeterminate sliding animation (ignores value). */\n  @Input({ transform: booleanAttribute }) indeterminate = false\n  /** Anchor the bar to the top or bottom of the viewport. */\n  @Input() position: 'top' | 'bottom' = 'top'\n  /** Show a spinner at the trailing edge of the bar. */\n  @Input({ transform: booleanAttribute }) spinner = false\n  /** Error state tints the bar (React `error` prop; the `error()` method is the imperative form). */\n  @Input({ alias: 'error', transform: booleanAttribute }) errorState = false\n  /** Hide the bar entirely (e.g. when finished). */\n  @Input({ transform: booleanAttribute }) hidden = false\n  @Input('class') className?: string\n  /** Fired with the new value on every internal update. */\n  @Output() readonly valueChange = new EventEmitter<number>()\n  /** Fired when progress reaches 100 (finish / fail). */\n  @Output('finish') readonly finished = new EventEmitter<void>()\n\n  readonly internal = signal(0)\n  /** Imperative fail() tints the bar without requiring the error input. */\n  private readonly internalError = signal(false)\n  /** After finish / fail, fade out then reset. */\n  private readonly fading = signal(false)\n  private raf: number | null = null\n  private hideTimer: ReturnType<typeof setTimeout> | null = null\n  /** Bumped on start / finish / fail so in-flight trickle frames abort. */\n  private generation = 0\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => this.clearTimers())\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    // Keep internal in sync when the controlled value input changes.\n    if (changes['value']) this.internal.set(this.value)\n  }\n\n  get pct(): number {\n    return Math.min(100, Math.max(0, this.internal()))\n  }\n\n  get isError(): boolean {\n    return this.errorState || this.internalError()\n  }\n\n  get barColor(): string {\n    return this.color || (this.isError ? 'var(--destructive)' : 'var(--primary)')\n  }\n\n  get visible(): boolean {\n    return !this.hidden && !this.fading() && (this.indeterminate || this.internal() > 0)\n  }\n\n  get hostClass(): string {\n    return cn(\n      'block pointer-events-none fixed left-0 z-[9999] w-full transition-opacity duration-300',\n      this.position === 'top' ? 'top-0' : 'bottom-0',\n      this.visible ? 'opacity-100' : 'opacity-0',\n      this.className,\n    )\n  }\n\n  private clearTimers(): void {\n    if (this.raf !== null) {\n      cancelAnimationFrame(this.raf)\n      this.raf = null\n    }\n    if (this.hideTimer) {\n      clearTimeout(this.hideTimer)\n      this.hideTimer = null\n    }\n  }\n\n  private emit(v: number): void {\n    this.valueChange.emit(v)\n  }\n\n  set(v: number): void {\n    this.internal.set(v)\n    this.emit(v)\n  }\n\n  /** Slowly creep the bar toward a soft ceiling so progress feels alive. */\n  private trickle(gen: number): void {\n    if (this.raf !== null) cancelAnimationFrame(this.raf)\n    const step = () => {\n      if (gen !== this.generation) return\n      const prev = this.internal()\n      if (prev >= 95) return\n      const next = Math.min(95, prev + (95 - prev) * 0.04 + 0.15)\n      this.internal.set(next)\n      this.emit(next)\n      if (next < 95 && gen === this.generation) this.raf = requestAnimationFrame(step)\n    }\n    this.raf = requestAnimationFrame(step)\n  }\n\n  start(from = 20): void {\n    this.clearTimers()\n    this.generation += 1\n    const gen = this.generation\n    this.internalError.set(false)\n    this.fading.set(false)\n    this.set(from)\n    this.trickle(gen)\n  }\n\n  inc(amount = 10): void {\n    const next = Math.min(99, this.internal() + amount)\n    this.internal.set(next)\n    this.emit(next)\n  }\n\n  finish(): void {\n    this.clearTimers()\n    this.generation += 1\n    this.internalError.set(false)\n    this.set(100)\n    this.finished.emit()\n    // Hold full bar briefly, then fade + reset so the next start() is clean.\n    this.hideTimer = setTimeout(() => {\n      this.fading.set(true)\n      this.hideTimer = setTimeout(() => {\n        this.internal.set(0)\n        this.emit(0)\n        this.fading.set(false)\n        this.hideTimer = null\n      }, 300)\n    }, 200)\n  }\n\n  fail(): void {\n    this.clearTimers()\n    this.generation += 1\n    this.internalError.set(true)\n    this.internal.set(100)\n    this.emit(100)\n    this.finished.emit()\n    this.hideTimer = setTimeout(() => {\n      this.fading.set(true)\n      this.hideTimer = setTimeout(() => {\n        this.internal.set(0)\n        this.internalError.set(false)\n        this.emit(0)\n        this.fading.set(false)\n        this.hideTimer = null\n      }, 300)\n    }, 400)\n  }\n\n  error(): void {\n    this.fail()\n  }\n}\n\nexport interface UseLoadingBar {\n  /** Pass the bar instance (e.g. from a `@ViewChild` setter). */\n  setRef: (bar: UiLoadingBarComponent | null | undefined) => void\n  loading: Signal<boolean>\n  isError: Signal<boolean>\n  start: (from?: number) => void\n  finish: () => void\n  error: () => void\n  inc: (amount?: number) => void\n  set: (value: number) => void\n}\n\n/**\n * Angular counterpart of React's `useLoadingBar` hook: drives a `<ui-loading-bar>` through\n * `setRef` and tracks `loading` / `isError` as signals.\n *\n *   bar = useLoadingBar()\n *   @ViewChild(UiLoadingBarComponent) set barRef(b: UiLoadingBarComponent) { this.bar.setRef(b) }\n *   this.bar.start(); await fetch(...); this.bar.finish()\n */\nexport function useLoadingBar(initial?: UiLoadingBarComponent | null): UseLoadingBar {\n  let handle: UiLoadingBarComponent | null = initial ?? null\n  const loading = signal(false)\n  const isError = signal(false)\n  return {\n    setRef: (bar) => {\n      handle = bar ?? null\n    },\n    loading: loading.asReadonly(),\n    isError: isError.asReadonly(),\n    start(from = 20) {\n      isError.set(false)\n      loading.set(true)\n      handle?.start(from)\n    },\n    finish() {\n      loading.set(false)\n      handle?.finish()\n    },\n    error() {\n      isError.set(true)\n      loading.set(false)\n      handle?.error()\n    },\n    inc(amount = 10) {\n      handle?.inc(amount)\n    },\n    set(value: number) {\n      handle?.set(value)\n    },\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/loading-bar/loading-bar.component.ts"
    },
    {
      "path": "packages/registry-angular/components/loading-bar/index.ts",
      "content": "export {\n  UiLoadingBarComponent,\n  useLoadingBar,\n  type LoadingBarHandle,\n  type UseLoadingBar,\n} from './loading-bar.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/loading-bar/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Top-of-viewport NProgress-style progress bar. Drive it imperatively via useLoadingBar() or the component methods (start / finish / error / inc / set), or with value / valueChange. Supports indeterminate mode, custom color and height, top/bottom anchoring, and an optional trailing spinner.",
  "categories": [
    "feedback"
  ]
}