{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "circular-progress",
  "title": "Circular Progress",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/circular-progress/CircularProgress.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface CircularProgressProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n    /** Progress value 0-100. Ignored when indeterminate is true. */\n    value?: number\n    /** Diameter in pixels. */\n    size?: 'sm' | 'default' | 'lg' | number\n    /** Stroke thickness in pixels. */\n    thickness?: number\n    /** Progress arc color. Defaults to primary. */\n    color?: string\n    /** Track (background ring) color. */\n    trackColor?: string\n    /** Indeterminate spinning mode. */\n    indeterminate?: boolean\n    /** Show the numeric value in the center. */\n    showValue?: boolean\n    /** Suffix appended to the value (e.g. '%'). */\n    suffix?: string\n    /** Accessible label. */\n    ariaLabel?: string\n    /** Center content. Receives `{ value }` (normalized 0-100). */\n    children?: Snippet<[{ value: number }]>\n  }\n</script>\n\n<script lang=\"ts\">\n  import { onDestroy } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { circularProgressVariants } from './circular-progress.variants'\n\n  let {\n    class: className,\n    value = 0,\n    size = 'default',\n    thickness = 8,\n    color,\n    trackColor,\n    indeterminate = false,\n    showValue = false,\n    suffix = '%',\n    ariaLabel = 'Progress',\n    children,\n    ...restProps\n  }: CircularProgressProps = $props()\n\n  const sizePx = $derived.by(() => {\n    if (typeof size === 'number') return size\n    switch (size) {\n      case 'sm':\n        return 40\n      case 'lg':\n        return 80\n      default:\n        return 56\n    }\n  })\n\n  const normalizedValue = $derived(Math.min(100, Math.max(0, value)))\n  const isComplete = $derived(!indeterminate && normalizedValue >= 100)\n\n  /** One-shot pulse only when value crosses into complete — not on static 100 mounts. */\n  let pulseComplete = $state(false)\n  let pulseTimer: ReturnType<typeof setTimeout> | undefined\n  let prevValue: number | undefined = undefined\n\n  $effect(() => {\n    const current = normalizedValue\n    const ind = indeterminate\n    if (ind || current < 100) {\n      pulseComplete = false\n      clearTimeout(pulseTimer)\n      prevValue = current\n      return\n    }\n    if (prevValue === undefined) {\n      prevValue = current\n      return\n    }\n    if (current >= 100 && prevValue < 100) {\n      pulseComplete = false\n      // Retrigger CSS animation if complete→incomplete→complete in quick succession.\n      requestAnimationFrame(() => {\n        pulseComplete = true\n        clearTimeout(pulseTimer)\n        pulseTimer = setTimeout(() => {\n          pulseComplete = false\n        }, 600)\n      })\n    }\n    prevValue = current\n  })\n\n  onDestroy(() => {\n    clearTimeout(pulseTimer)\n  })\n\n  const radius = $derived((sizePx - thickness) / 2)\n  const circumference = $derived(2 * Math.PI * radius)\n  const strokeDashoffset = $derived(indeterminate ? circumference * 0.25 : circumference * (1 - normalizedValue / 100))\n\n  const resolvedColor = $derived(color || 'var(--primary)')\n  const resolvedTrackColor = $derived(trackColor || 'var(--muted)')\n\n  const viewBox = $derived(`0 0 ${sizePx} ${sizePx}`)\n  const center = $derived(sizePx / 2)\n\n  const fontSize = $derived.by(() => {\n    const s = sizePx\n    if (s <= 40) return 'text-xs'\n    if (s <= 56) return 'text-sm'\n    return 'text-base'\n  })\n</script>\n\n<div\n  data-uipkge\n  data-slot=\"circular-progress\"\n  data-size={typeof size === 'string' ? size : 'custom'}\n  data-indeterminate={indeterminate ? 'true' : 'false'}\n  data-complete={isComplete ? 'true' : 'false'}\n  class={cn(circularProgressVariants(), className)}\n  style=\"width: {sizePx}px; height: {sizePx}px;\"\n  role=\"progressbar\"\n  aria-valuemin={0}\n  aria-valuemax={100}\n  aria-valuenow={indeterminate ? undefined : normalizedValue}\n  aria-busy={indeterminate ? 'true' : undefined}\n  aria-label={ariaLabel}\n  {...restProps}\n>\n  <svg width={sizePx} height={sizePx} {viewBox} class=\"block\">\n    <!-- Track -->\n    <circle cx={center} cy={center} r={radius} fill=\"none\" stroke={resolvedTrackColor} stroke-width={thickness} />\n    <!-- Progress arc -->\n    <g\n      transform={indeterminate ? undefined : `rotate(-90 ${center} ${center})`}\n      class={indeterminate ? 'animate-spin-circular' : ''}\n      style={indeterminate ? 'transform-box: fill-box; transform-origin: center;' : undefined}\n    >\n      <circle\n        cx={center}\n        cy={center}\n        r={radius}\n        fill=\"none\"\n        stroke={resolvedColor}\n        stroke-width={thickness}\n        stroke-linecap=\"round\"\n        stroke-dasharray={circumference}\n        stroke-dashoffset={strokeDashoffset}\n        class={cn(\n          !indeterminate && 'transition-[stroke-dashoffset] duration-500 ease-out motion-reduce:transition-none',\n          pulseComplete && 'animate-circular-complete',\n        )}\n      />\n    </g>\n  </svg>\n\n  {#if showValue || children}\n    <div class=\"absolute inset-0 flex items-center justify-center\">\n      {#if children}\n        {@render children({ value: normalizedValue })}\n      {:else if showValue}\n        <span class={cn('text-foreground font-medium tabular-nums', fontSize)}>\n          {Math.round(normalizedValue)}{suffix}\n        </span>\n      {/if}\n    </div>\n  {/if}\n</div>\n\n<style>\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</style>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/circular-progress/CircularProgress.svelte"
    },
    {
      "path": "packages/registry-svelte/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/lib/components/ui/circular-progress/circular-progress.variants.ts"
    },
    {
      "path": "packages/registry-svelte/components/circular-progress/index.ts",
      "content": "export { default as CircularProgress, type CircularProgressProps } from './CircularProgress.svelte'\n\n// Re-export variant API from the sibling file (kept separate to avoid the\n// CircularProgress.svelte <-> index.ts circular import that broke dev SSR for Card).\nexport { circularProgressVariants, type CircularProgressVariants } from './circular-progress.variants'\n",
      "type": "registry:ui",
      "target": "~/src/lib/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 children snippet for center content, and a show-value prop that renders the percentage in the center.",
  "categories": [
    "feedback",
    "display"
  ]
}