{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "circular-progress",
  "title": "Circular Progress",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/circular-progress/circular-progress.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  DestroyRef,\n  ElementRef,\n  Input,\n  OnChanges,\n  ViewChild,\n  ViewEncapsulation,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { circularProgressVariants, type CircularProgressVariants } from './circular-progress.variants'\n\nexport type CircularProgressSize = 'sm' | 'default' | 'lg' | number\n\nconst sizePxMap = { sm: 40, default: 56, lg: 80 } as const\n\n// Copied verbatim from React CircularProgress (injected there as an inline <style>). Unscoped\n// (ViewEncapsulation.None) so the animation classes match the svg nodes in the template.\nconst CIRCULAR_PROGRESS_STYLES = `\n@media (prefers-reduced-motion: no-preference) {\n  .animate-spin-circular {\n    animation: spin-circular 1.4s linear infinite;\n  }\n\n  /* Soft acknowledge when the arc lands on 100 — one shot per enter. */\n  .animate-circular-complete {\n    animation: circular-progress-complete 0.55s ease-out 1;\n  }\n}\n\n@keyframes spin-circular {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n@keyframes circular-progress-complete {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  45% {\n    opacity: 0.72;\n  }\n}\n`\n\n/**\n * Angular port of UIPKGE CircularProgress (React `CircularProgress`). Radial progressbar:\n * value 0-100 (clamped), size preset or pixels, stroke thickness, arc / track colors, an\n * indeterminate mode that spins a partial arc, `showValue` for a centered percentage, and a\n * one-shot pulse when the value crosses into 100. Projected content replaces the centered\n * value (React `children`).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-circular-progress, [ui-circular-progress]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  styles: [CIRCULAR_PROGRESS_STYLES],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"circular-progress\"',\n    '[attr.data-size]': 'sizeLabel',\n    '[attr.data-indeterminate]': 'indeterminate ? \"true\" : \"false\"',\n    '[attr.data-complete]': 'isComplete ? \"true\" : \"false\"',\n    '[class]': 'hostClass',\n    '[style.width.px]': 'sizePx',\n    '[style.height.px]': 'sizePx',\n    '[attr.role]': '\"progressbar\"',\n    '[attr.aria-valuemin]': '0',\n    '[attr.aria-valuemax]': '100',\n    '[attr.aria-valuenow]': 'indeterminate ? null : normalizedValue',\n    '[attr.aria-busy]': 'indeterminate ? \"true\" : null',\n    '[attr.aria-label]': 'ariaLabel',\n  },\n  template: `\n    <svg [attr.width]=\"sizePx\" [attr.height]=\"sizePx\" [attr.viewBox]=\"viewBox\" class=\"block\">\n      <circle\n        [attr.cx]=\"center\"\n        [attr.cy]=\"center\"\n        [attr.r]=\"radius\"\n        fill=\"none\"\n        [attr.stroke]=\"resolvedTrackColor\"\n        [attr.stroke-width]=\"thickness\"\n      />\n      <g\n        [attr.transform]=\"indeterminate ? null : 'rotate(-90 ' + center + ' ' + center + ')'\"\n        [attr.class]=\"indeterminate ? 'animate-spin-circular' : ''\"\n        [style.transform-box]=\"indeterminate ? 'fill-box' : null\"\n        [style.transform-origin]=\"indeterminate ? 'center' : null\"\n      >\n        <circle\n          [attr.cx]=\"center\"\n          [attr.cy]=\"center\"\n          [attr.r]=\"radius\"\n          fill=\"none\"\n          [attr.stroke]=\"resolvedColor\"\n          [attr.stroke-width]=\"thickness\"\n          stroke-linecap=\"round\"\n          [attr.stroke-dasharray]=\"circumference\"\n          [attr.stroke-dashoffset]=\"strokeDashoffset\"\n          [attr.class]=\"arcClass\"\n        />\n      </g>\n    </svg>\n    <div\n      #overlay\n      class=\"absolute inset-0 flex items-center justify-center\"\n      [attr.hidden]=\"showValue || hasChildren() ? null : ''\"\n    >\n      <ng-content />\n      @if (showValue && !hasChildren()) {\n        <span #valueLabel [class]=\"valueClass\">{{ roundedValue }}{{ suffix }}</span>\n      }\n    </div>\n  `,\n})\nexport class UiCircularProgressComponent implements OnChanges, AfterViewInit {\n  @Input() value = 0\n  @Input() size: CircularProgressSize = 'default'\n  @Input() thickness = 8\n  @Input() color?: string\n  @Input() trackColor?: string\n  @Input({ transform: booleanAttribute }) indeterminate = false\n  @Input({ transform: booleanAttribute }) showValue = false\n  @Input() suffix = '%'\n  @Input() ariaLabel = 'Progress'\n  @Input('class') className?: string\n\n  @ViewChild('overlay', { static: true }) private overlay?: ElementRef<HTMLElement>\n  @ViewChild('valueLabel') private valueLabel?: ElementRef<HTMLElement>\n\n  /** True when content is projected (React `children`), which replaces the value label. */\n  readonly hasChildren = signal(false)\n  /** One-shot pulse only when value crosses into complete — not on static 100 mounts. */\n  readonly pulseComplete = signal(false)\n  private prevValue: number | undefined\n  private raf = 0\n  private timer: ReturnType<typeof setTimeout> | undefined\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => this.clearPulse())\n  }\n\n  ngAfterViewInit(): void {\n    const own = this.valueLabel?.nativeElement\n    const nodes = [...(this.overlay?.nativeElement.childNodes ?? [])].filter((n) => n !== own)\n    this.hasChildren.set(\n      nodes.some((n) => (n.nodeType === Node.TEXT_NODE ? !!n.textContent?.trim() : n.nodeType === Node.ELEMENT_NODE)),\n    )\n  }\n\n  ngOnChanges(): void {\n    const v = this.normalizedValue\n    if (this.indeterminate || v < 100) {\n      this.clearPulse()\n      this.pulseComplete.set(false)\n      this.prevValue = v\n      return\n    }\n    const prev = this.prevValue\n    this.prevValue = v\n    if (prev === undefined || prev >= 100) return\n    this.clearPulse()\n    this.pulseComplete.set(false)\n    if (typeof requestAnimationFrame !== 'undefined')\n      this.raf = requestAnimationFrame(() => this.pulseComplete.set(true))\n    this.timer = setTimeout(() => this.pulseComplete.set(false), 600)\n  }\n\n  private clearPulse(): void {\n    if (this.raf && typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(this.raf)\n    this.raf = 0\n    if (this.timer) clearTimeout(this.timer)\n    this.timer = undefined\n  }\n\n  get sizePx(): number {\n    if (typeof this.size === 'number') return this.size\n    return sizePxMap[this.size] ?? 56\n  }\n\n  get sizeLabel(): string {\n    return typeof this.size === 'string' ? this.size : 'custom'\n  }\n\n  get normalizedValue(): number {\n    return Math.min(100, Math.max(0, this.value))\n  }\n\n  get isComplete(): boolean {\n    return !this.indeterminate && this.normalizedValue >= 100\n  }\n\n  get radius(): number {\n    return (this.sizePx - this.thickness) / 2\n  }\n\n  get circumference(): number {\n    return 2 * Math.PI * this.radius\n  }\n\n  get strokeDashoffset(): number {\n    if (this.indeterminate) return this.circumference * 0.25\n    return this.circumference * (1 - this.normalizedValue / 100)\n  }\n\n  get resolvedColor(): string {\n    return this.color || 'var(--primary)'\n  }\n\n  get resolvedTrackColor(): string {\n    return this.trackColor || 'var(--muted)'\n  }\n\n  get viewBox(): string {\n    return `0 0 ${this.sizePx} ${this.sizePx}`\n  }\n\n  get center(): number {\n    return this.sizePx / 2\n  }\n\n  get roundedValue(): number {\n    return Math.round(this.normalizedValue)\n  }\n\n  get valueClass(): string {\n    const s = this.sizePx\n    const font = s <= 40 ? 'text-xs' : s <= 56 ? 'text-sm' : 'text-base'\n    return cn('text-foreground font-medium tabular-nums', font)\n  }\n\n  get hostClass(): string {\n    return cn(circularProgressVariants({} as CircularProgressVariants), this.className)\n  }\n\n  get arcClass(): string {\n    return cn(\n      !this.indeterminate && 'transition-[stroke-dashoffset] duration-500 ease-out motion-reduce:transition-none',\n      this.pulseComplete() && 'animate-circular-complete',\n    )\n  }\n}\n\nexport { circularProgressVariants, type CircularProgressVariants }\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/circular-progress/circular-progress.component.ts"
    },
    {
      "path": "packages/registry-angular/components/circular-progress/circular-progress.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so components can import `circularProgressVariants` from './circular-progress.variants' without creating a circular dependency through the index.\n */\nexport const circularProgressVariants = cva('relative inline-flex items-center justify-center')\n\nexport type CircularProgressVariants = VariantProps<typeof circularProgressVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/circular-progress/circular-progress.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/circular-progress/index.ts",
      "content": "export { UiCircularProgressComponent, circularProgressVariants } from './circular-progress.component'\nexport type { CircularProgressSize, CircularProgressVariants } from './circular-progress.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/circular-progress/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Radial/circular progress indicator. Supports value (0-100), size presets or custom pixel size, stroke thickness, custom arc and track colors, indeterminate spinning mode, a label slot for center content, and a show-value prop that renders the percentage in the center.",
  "categories": [
    "feedback",
    "display"
  ]
}