{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "loading-bar",
  "title": "Loading Bar",
  "type": "registry:ui",
  "description": "Top-of-viewport NProgress-style progress bar. Drive it imperatively via the useLoadingBar hook (start/finish/error/inc) or with value/onChange. Supports indeterminate mode, custom color and height, top/bottom anchoring, and an optional trailing spinner.",
  "categories": [
    "feedback"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/loading-bar/loading-bar.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/**\n * <uip-loading-bar> — the registry LoadingBar as a web component.\n *\n * Class strings are React's verbatim, on the same elements (viewport-pinned\n * root, track, determinate fill / indeterminate slider, trailing spinner).\n * The host is `display: contents` — the bar positions itself `fixed`, like\n * React. React's injected keyframes (loading-bar-slide) are copied verbatim\n * into the static styles — the one allowed CSS exception for React-injected\n * keyframes.\n *\n * Drive it imperatively (`start()` / `finish()` / `fail()` / `inc()` / `set()` —\n * React's `useLoadingBar` handle, minus the hook) or bind `value` directly.\n * React's `hidden` prop is the native `hidden` attribute (the bar fades out,\n * like React's opacity-0). One rename: the boolean state prop keeps the name\n * `error`, so React's `error()` handle method is `fail()` only (React aliases\n * `error: fail` anyway).\n *\n * Events (bubble, composed): `value-change` (detail: { value } — React's\n * `onValueChange`), `input` + `change` (value on `.value`, like other\n * value-bearing elements), `finish` (detail: { value } — React's `onFinish`).\n *\n * Parts: `base` (the bar root), `fill` (determinate fill / indeterminate\n * slider), `spinner`.\n */\nexport class UipLoadingBar extends LitElement {\n  // The bar positions itself (fixed); the host adds no box.\n  static styles = [\n    tailwind,\n    css`:host { display: contents; }`,\n    // React's injected <style>, verbatim.\n    css`\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% {\n          left: -33%;\n        }\n        100% {\n          left: 100%;\n        }\n      }\n    `,\n  ]\n\n  static properties = {\n    value: { type: Number },\n    color: {},\n    height: { type: Number },\n    indeterminate: { type: Boolean, reflect: true },\n    position: { reflect: true },\n    spinner: { type: Boolean, reflect: true },\n    error: { type: Boolean, reflect: true },\n    internal: { state: true },\n    internalError: { state: true },\n    fading: { state: true },\n  }\n\n  value = 0\n  color = ''\n  height = 3\n  indeterminate = false\n  position: 'top' | 'bottom' = 'top'\n  spinner = false\n  error = false\n  private internal = 0\n  /** Imperative fail() tints the bar without requiring the error prop. */\n  private internalError = false\n  /** After finish/fail, fade out then reset. */\n  private fading = false\n  private raf: number | null = null\n  private hideTimer?: ReturnType<typeof setTimeout>\n  /** Bumped on start/finish/fail so in-flight trickle frames abort. */\n  private generation = 0\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', 'loading-bar')\n    this.internal = this.value\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.clearTimers()\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    // Keep internal in sync when the controlled value prop changes.\n    if (changed.has('value')) this.internal = this.value\n  }\n\n  private clearTimers() {\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 = undefined\n    }\n  }\n\n  private emit(v: number) {\n    this.internal = v\n    this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('value-change', { detail: { value: v }, bubbles: true, composed: true }))\n  }\n\n  /** Slowly creep the bar toward a soft ceiling so progress feels alive. */\n  private trickle(gen: number) {\n    if (this.raf !== null) cancelAnimationFrame(this.raf)\n    const step = () => {\n      if (gen !== this.generation) return\n      if (this.internal >= 95) return\n      const next = Math.min(95, this.internal + (95 - this.internal) * 0.04 + 0.15)\n      this.emit(next)\n      if (next < 95 && gen === this.generation) this.raf = requestAnimationFrame(step)\n      else this.raf = null\n    }\n    this.raf = requestAnimationFrame(step)\n  }\n\n  /** Start the bar at `from` (default 20) and trickle toward 95. */\n  start(from = 20) {\n    this.clearTimers()\n    this.generation += 1\n    const gen = this.generation\n    this.internalError = false\n    this.fading = false\n    this.emit(from)\n    this.trickle(gen)\n  }\n\n  /** Nudge the bar forward by `amount` (default 10), capped at 99. */\n  inc(amount = 10) {\n    this.emit(Math.min(99, this.internal + amount))\n  }\n\n  /** Set the bar to an exact value. */\n  set(v: number) {\n    this.emit(v)\n  }\n\n  /** Complete the bar: fill to 100, hold briefly, fade out, reset. */\n  finish() {\n    this.clearTimers()\n    this.generation += 1\n    this.internalError = false\n    this.emit(100)\n    this.dispatchEvent(new CustomEvent('finish', { detail: { value: 100 }, bubbles: true, composed: true }))\n    // Hold full bar briefly, then fade + reset so the next start() is clean.\n    this.hideTimer = setTimeout(() => {\n      this.fading = true\n      this.hideTimer = setTimeout(() => {\n        this.emit(0)\n        this.fading = false\n        this.hideTimer = undefined\n      }, 300)\n    }, 200)\n  }\n\n  /** Fail the bar: tint destructive, hold, fade out, reset. */\n  fail() {\n    this.clearTimers()\n    this.generation += 1\n    this.internalError = true\n    this.emit(100)\n    this.dispatchEvent(new CustomEvent('finish', { detail: { value: 100 }, bubbles: true, composed: true }))\n    this.hideTimer = setTimeout(() => {\n      this.fading = true\n      this.hideTimer = setTimeout(() => {\n        this.emit(0)\n        this.internalError = false\n        this.fading = false\n        this.hideTimer = undefined\n      }, 300)\n    }, 400)\n  }\n\n  render() {\n    const pct = Math.min(100, Math.max(0, this.internal))\n    const isError = this.error || this.internalError\n    const barColor = this.color || (isError ? 'var(--destructive)' : 'var(--primary)')\n    // `hidden` is the native host attribute (no `hidden` property — it already\n    // exists on HTMLElement); like React it fades the bar out.\n    const visible = !this.hasAttribute('hidden') && !this.fading && (this.indeterminate || this.internal > 0)\n    const spin = this.spinner\n      ? html`<div\n          part=\"spinner\"\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=${styleMap({ color: barColor })}\n        ></div>`\n      : nothing\n\n    return html`<div\n      part=\"base\"\n      data-uipkge=\"\"\n      data-slot=\"loading-bar\"\n      data-position=${this.position}\n      data-state=${isError ? 'error' : this.indeterminate ? 'indeterminate' : 'determinate'}\n      class=${cn(\n        'pointer-events-none fixed left-0 z-[9999] w-full transition-opacity duration-300',\n        this.position === 'top' ? 'top-0' : 'bottom-0',\n        visible ? 'opacity-100' : 'opacity-0',\n      )}\n      style=${styleMap({ height: `${this.height}px` })}\n      role=\"progressbar\"\n      aria-valuemin=\"0\"\n      aria-valuemax=\"100\"\n      aria-valuenow=${this.indeterminate ? nothing : pct}\n      aria-busy=${visible && !isError ? 'true' : nothing}\n      aria-hidden=${!visible ? 'true' : nothing}\n    >\n      <div class=\"absolute inset-0 bg-transparent\"></div>\n      ${this.indeterminate\n        ? html`<div\n            part=\"fill\"\n            data-slot=\"loading-bar-indeterminate\"\n            class=\"loading-bar-indeterminate absolute inset-y-0 w-1/3\"\n            style=${styleMap({ backgroundColor: barColor })}\n          >\n            ${spin}\n          </div>`\n        : html`<div\n            part=\"fill\"\n            data-slot=\"loading-bar-fill\"\n            class=\"absolute inset-y-0 left-0 transition-[width] duration-200 ease-out\"\n            style=${styleMap({ width: `${pct}%`, backgroundColor: barColor })}\n          >\n            ${spin}\n          </div>`}\n    </div>`\n  }\n}\n\ncustomElements.get('uip-loading-bar') || customElements.define('uip-loading-bar', UipLoadingBar)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-loading-bar': UipLoadingBar\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/loading-bar/loading-bar.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
