{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "RefineFrame-TS-CSS",
	"title": "RefineFrame",
	"description": "Media frame that steps an image through generation stages without any layout shift.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "RefineFrame.css",
			"target": "@components/RefineFrame.css",
			"content": ".refine-frame {\n  --rf-w: 320px;\n  --rf-aspect: 4 / 3;\n  --rf-radius: 16px;\n  --rf-bg: #27272a;\n  --rf-ink: #f5f5f5;\n  --rf-stage: 400ms;\n  --rf-blur: 1.5px;\n  --rf-sat: 0.8;\n  --rf-scale: 1.02;\n  --rf-opacity: 0.85;\n  --rf-error: #ef4444;\n  --rf-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  position: relative;\n  isolation: isolate;\n  width: min(var(--rf-w), 100%);\n  aspect-ratio: var(--rf-aspect);\n  border-radius: var(--rf-radius);\n  overflow: hidden;\n  background: var(--rf-bg);\n  color: var(--rf-ink);\n  font-family: inherit;\n  font-size: 12px;\n  font-weight: 500;\n  line-height: 1;\n}\n\n.refine-frame__media {\n  position: absolute;\n  inset: 0;\n  opacity: var(--rf-opacity);\n  filter: blur(var(--rf-blur)) saturate(var(--rf-sat));\n  transform: scale(var(--rf-scale));\n  transition:\n    opacity var(--rf-stage) var(--rf-ease-out),\n    filter var(--rf-stage) var(--rf-ease-out),\n    transform var(--rf-stage) var(--rf-ease-out);\n}\n\n.refine-frame[data-status='queued'] .refine-frame__media {\n  animation: refine-frame-wait 2.4s ease-in-out infinite;\n}\n\n@keyframes refine-frame-wait {\n  0%,\n  100% {\n    opacity: 0.45;\n  }\n\n  50% {\n    opacity: 0.65;\n  }\n}\n\n.refine-frame__print {\n  width: 100%;\n  height: 100%;\n  transition: opacity var(--rf-stage) ease;\n}\n\n.refine-frame__print > * {\n  display: block;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.refine-frame[data-mosaic] .refine-frame__print {\n  opacity: 0;\n}\n\n.refine-frame[data-resolved] .refine-frame__print {\n  opacity: 1;\n}\n\n.refine-frame__mosaic {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity var(--rf-stage) ease;\n}\n\n.refine-frame[data-mosaic] .refine-frame__mosaic {\n  opacity: 1;\n}\n\n.refine-frame[data-resolved] .refine-frame__mosaic {\n  opacity: 0;\n}\n\n.refine-frame__sweep {\n  position: absolute;\n  inset: 0;\n  background: linear-gradient(\n    115deg,\n    transparent 38%,\n    color-mix(in srgb, var(--rf-ink) 14%, transparent) 50%,\n    transparent 62%\n  );\n  background-size: 260% 100%;\n  opacity: 0;\n  pointer-events: none;\n  transition: opacity var(--rf-stage) ease;\n}\n\n.refine-frame[data-sweep]:not([data-mosaic]) .refine-frame__sweep {\n  opacity: 1;\n  animation: refine-frame-sweep 2.2s linear infinite;\n}\n\n@keyframes refine-frame-sweep {\n  from {\n    background-position: 130% 0;\n  }\n\n  to {\n    background-position: -130% 0;\n  }\n}\n\n.refine-frame__chip {\n  position: absolute;\n  bottom: 10px;\n  left: 10px;\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  height: 26px;\n  padding: 0 10px 0 8px;\n  border-radius: 13px;\n  background: color-mix(in srgb, var(--rf-bg) 72%, transparent);\n  backdrop-filter: blur(8px);\n  -webkit-backdrop-filter: blur(8px);\n  opacity: 1;\n  transform: translateY(0);\n  pointer-events: none;\n  transition:\n    opacity 200ms ease,\n    transform 200ms var(--rf-ease-out);\n}\n\n@starting-style {\n  .refine-frame__chip {\n    opacity: 0;\n    transform: translateY(4px);\n  }\n}\n\n.refine-frame__mark {\n  display: inline-flex;\n  color: color-mix(in srgb, var(--rf-ink) 80%, transparent);\n}\n\n.refine-frame__mark[data-kind='spin'] {\n  animation: refine-frame-spin 1.1s linear infinite;\n}\n\n.refine-frame__mark[data-kind='error'] {\n  color: var(--rf-error);\n}\n\n@keyframes refine-frame-spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.refine-frame__label {\n  animation: refine-frame-label 200ms ease both;\n}\n\n@keyframes refine-frame-label {\n  from {\n    opacity: 0;\n    filter: blur(2px);\n  }\n}\n\n.refine-frame__retry {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  height: 32px;\n  padding: 0 14px 0 12px;\n  border: 0;\n  border-radius: 16px;\n  background: color-mix(in srgb, var(--rf-ink) 14%, var(--rf-bg));\n  color: var(--rf-ink);\n  font: inherit;\n  font-size: 13px;\n  font-weight: 500;\n  cursor: pointer;\n  outline: none;\n  opacity: 1;\n  transform: translate(-50%, -50%) scale(1);\n  -webkit-tap-highlight-color: transparent;\n  transition:\n    opacity 200ms ease,\n    transform 160ms var(--rf-ease-out),\n    background-color 150ms ease;\n}\n\n@starting-style {\n  .refine-frame__retry {\n    opacity: 0;\n    transform: translate(-50%, -50%) scale(0.96);\n  }\n}\n\n.refine-frame__retry:active {\n  transform: translate(-50%, -50%) scale(0.96);\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .refine-frame__retry:hover {\n    background: color-mix(in srgb, var(--rf-ink) 20%, var(--rf-bg));\n  }\n}\n\n.refine-frame__sr {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .refine-frame__media {\n    transition: opacity var(--rf-stage) ease;\n  }\n\n  .refine-frame[data-sweep] .refine-frame__sweep {\n    animation: none;\n    opacity: 0;\n  }\n\n  .refine-frame[data-status='queued'] .refine-frame__media {\n    animation: none;\n  }\n\n  .refine-frame__mark[data-kind='spin'] {\n    animation: none;\n  }\n\n  .refine-frame__chip,\n  .refine-frame__retry {\n    transition: opacity 200ms ease;\n  }\n\n  .refine-frame__retry:active {\n    transform: translate(-50%, -50%);\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "RefineFrame.tsx",
			"content": "'use client';\n\nimport React, { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Alert02Icon, Loading03Icon, RefreshIcon, Tick02Icon } from '@hugeicons/core-free-icons';\nimport './RefineFrame.css';\n\nexport type RefineFrameStatus = 'queued' | 'generating' | 'refining' | 'complete' | 'error';\n\nexport interface RefineFrameProps {\n  status?: RefineFrameStatus;\n  children?: ReactNode;\n  aspectRatio?: string;\n  width?: number;\n  radius?: number;\n  background?: string;\n  color?: string;\n  stageDuration?: number;\n  sweep?: boolean;\n  showStatus?: boolean;\n  hideAfter?: number;\n  labels?: Partial<Record<RefineFrameStatus, string>>;\n  retryLabel?: string;\n  onRetry?: () => void;\n  className?: string;\n}\n\ntype Stage = { blur: number; sat: number; scale: number; opacity: number };\ninterface Sim {\n  p: number;\n  raf: number;\n  last: number;\n  key: string;\n  w: number;\n  h: number;\n  levels: HTMLCanvasElement[];\n  glint: CanvasGradient | null;\n  sent: boolean;\n}\ninterface Live {\n  status: RefineFrameStatus;\n  stageDuration: number;\n  sweep: boolean;\n  reduce: boolean;\n}\n\nconst STAGES: Record<RefineFrameStatus, Stage> = {\n  queued: { blur: 4, sat: 0.6, scale: 1.04, opacity: 0.55 },\n  generating: { blur: 1.5, sat: 0.8, scale: 1.02, opacity: 0.85 },\n  refining: { blur: 0.5, sat: 0.95, scale: 1.005, opacity: 1 },\n  complete: { blur: 0, sat: 1, scale: 1, opacity: 1 },\n  error: { blur: 2, sat: 0.5, scale: 1, opacity: 0.28 }\n};\nconst TARGET: Partial<Record<RefineFrameStatus, number>> = { queued: 0, generating: 0.5, refining: 0.875, complete: 1 };\nconst LEVELS = [48, 32, 20, 12, 8, 5, 3, 2, 1];\nconst EDGE = 28;\nconst STRIPS = 14;\nconst DEFAULT_LABELS: Record<RefineFrameStatus, string> = {\n  queued: 'Queued',\n  generating: 'Generating',\n  refining: 'Refining',\n  complete: 'Ready',\n  error: 'Failed'\n};\nconst ACTIVE = new Set<string>(['queued', 'generating', 'refining']);\n\nconst build = (s: Sim, canvas: HTMLCanvasElement, img: HTMLImageElement, dpr: number) => {\n  const rect = canvas.getBoundingClientRect();\n  const W = Math.max(1, Math.round(rect.width * dpr));\n  const H = Math.max(1, Math.round(rect.height * dpr));\n  const key = `${img.currentSrc}|${W}x${H}`;\n  if (s.key === key) return;\n  s.key = key;\n  s.w = W;\n  s.h = H;\n  canvas.width = W;\n  canvas.height = H;\n  const iw = img.naturalWidth;\n  const ih = img.naturalHeight;\n  const cover = Math.max(W / iw, H / ih);\n  const sw = W / cover;\n  const sh = H / cover;\n  const sx = (iw - sw) / 2;\n  const sy = (ih - sh) / 2;\n  const glint = canvas.getContext('2d')?.createLinearGradient(0, 0, W, 0) ?? null;\n  if (glint) {\n    for (const [at, a] of [\n      [0, 0],\n      [0.08, 0.1],\n      [0.2, 0.7],\n      [0.32, 1],\n      [0.68, 1],\n      [0.8, 0.7],\n      [0.92, 0.1],\n      [1, 0]\n    ]) {\n      glint.addColorStop(at, `rgba(255, 255, 255, ${a})`);\n    }\n  }\n  s.glint = glint;\n  s.levels = LEVELS.map(block => {\n    const b = block === 1 ? 1 : Math.max(2, Math.round(block * dpr));\n    const full = document.createElement('canvas');\n    full.width = W;\n    full.height = H;\n    const fc = full.getContext('2d');\n    if (!fc) return full;\n    if (b === 1) {\n      fc.imageSmoothingEnabled = true;\n      fc.imageSmoothingQuality = 'high';\n      fc.drawImage(img, sx, sy, sw, sh, 0, 0, W, H);\n      return full;\n    }\n    const small = document.createElement('canvas');\n    small.width = Math.max(1, Math.round(W / b));\n    small.height = Math.max(1, Math.round(H / b));\n    const sc = small.getContext('2d');\n    if (sc) {\n      sc.imageSmoothingEnabled = true;\n      sc.imageSmoothingQuality = 'high';\n      sc.drawImage(img, sx, sy, sw, sh, 0, 0, small.width, small.height);\n    }\n    fc.imageSmoothingEnabled = false;\n    fc.drawImage(small, 0, 0, W, H);\n    return full;\n  });\n};\n\nconst RefineFrame: React.FC<RefineFrameProps> = ({\n  status = 'generating',\n  children,\n  aspectRatio = '4 / 3',\n  width = 320,\n  radius = 16,\n  background = '#27272a',\n  color = '#f5f5f5',\n  stageDuration = 400,\n  sweep = true,\n  showStatus = true,\n  hideAfter = 1200,\n  labels = DEFAULT_LABELS,\n  retryLabel = 'Retry',\n  onRetry,\n  className = ''\n}) => {\n  const stage = STAGES[status] ?? STAGES.generating;\n  const active = ACTIVE.has(status);\n  const text = { ...DEFAULT_LABELS, ...labels };\n  const printRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const sim = useRef<Sim>({ p: 0, raf: 0, last: 0, key: '', w: 0, h: 0, levels: [], glint: null, sent: false });\n  const live = useRef<Live>({} as Live);\n  live.current = { status, stageDuration, sweep, reduce: false };\n  const [mosaic, setMosaic] = useState(false);\n  const [resolved, setResolved] = useState(false);\n\n  const [chip, setChip] = useState(showStatus);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => {\n    clearTimeout(timer.current);\n    if (!showStatus) {\n      setChip(false);\n      return undefined;\n    }\n    setChip(true);\n    if (status === 'complete' && hideAfter > 0) {\n      timer.current = setTimeout(() => setChip(false), hideAfter);\n    }\n    return () => clearTimeout(timer.current);\n  }, [status, showStatus, hideAfter]);\n\n  const tick = (now: number) => {\n    const s = sim.current;\n    const c = live.current;\n    const canvas = canvasRef.current;\n    const img = printRef.current?.querySelector('img') as HTMLImageElement | null;\n    if (!canvas || !img || !img.naturalWidth) {\n      s.raf = 0;\n      return;\n    }\n    const dt = Math.min(0.05, s.last ? (now - s.last) / 1000 : 0.016);\n    s.last = now;\n    const dpr = Math.min(2, window.devicePixelRatio || 1);\n    build(s, canvas, img, dpr);\n    const n = s.levels.length - 1;\n    const target = TARGET[c.status] ?? s.p;\n    const rate = c.reduce ? 1e9 : 1 / (n * (c.stageDuration / 1000));\n    const step = rate * dt;\n    if (target < s.p) s.p = target;\n    else if (target - s.p <= step) s.p = target;\n    else s.p += step;\n    const ctx = canvas.getContext('2d');\n    if (ctx) {\n      const L = s.p * n;\n      const i = Math.min(n, Math.floor(L + 1e-6));\n      const frac = L - i;\n      ctx.globalAlpha = 1;\n      ctx.drawImage(s.levels[i], 0, 0);\n      if (i < n && frac > 0) {\n        const edge = EDGE * dpr;\n        const front = frac * (s.h + edge) - edge / 2;\n        const top = Math.max(0, Math.floor(front - edge / 2));\n        if (top > 0) ctx.drawImage(s.levels[i + 1], 0, 0, s.w, top, 0, 0, s.w, top);\n        const sh = edge / STRIPS;\n        for (let k = 0; k < STRIPS; k += 1) {\n          const y = front - edge / 2 + k * sh;\n          if (y + sh <= 0 || y >= s.h) continue;\n          const t = 1 - (k + 0.5) / STRIPS;\n          ctx.globalAlpha = t * t * (3 - 2 * t);\n          const y0 = Math.max(0, y);\n          const h0 = Math.min(s.h, y + sh) - y0;\n          if (h0 > 0) ctx.drawImage(s.levels[i + 1], 0, y0, s.w, h0, 0, y0, s.w, h0);\n        }\n        ctx.globalAlpha = 1;\n        if (c.sweep && !c.reduce && s.glint && front > 0 && front < s.h) {\n          ctx.fillStyle = s.glint;\n          ctx.globalAlpha = 0.12;\n          ctx.fillRect(0, front - 2 * dpr, s.w, 4 * dpr);\n          ctx.globalAlpha = 0.3;\n          ctx.fillRect(0, front - dpr, s.w, 2 * dpr);\n          ctx.globalAlpha = 1;\n        }\n      }\n    }\n    const done = s.p >= 1;\n    if (done !== s.sent) {\n      s.sent = done;\n      setResolved(done);\n    }\n    const keep = (!c.reduce && ACTIVE.has(c.status)) || Math.abs(target - s.p) > 0.0005;\n    s.raf = keep ? requestAnimationFrame(tick) : 0;\n    if (!keep) s.last = 0;\n  };\n  const wake = () => {\n    const s = sim.current;\n    if (!s.raf) s.raf = requestAnimationFrame(tick);\n  };\n\n  useEffect(() => {\n    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const sync = () => {\n      live.current.reduce = mq.matches;\n    };\n    sync();\n    mq.addEventListener('change', sync);\n    return () => mq.removeEventListener('change', sync);\n  }, []);\n\n  useEffect(() => {\n    const img = printRef.current?.querySelector('img') as HTMLImageElement | null;\n    if (!img) {\n      setMosaic(false);\n      return undefined;\n    }\n    let gone = false;\n    const start = () => {\n      if (gone) return;\n      setMosaic(true);\n      wake();\n    };\n    if (img.complete && img.naturalWidth) start();\n    else img.addEventListener('load', start, { once: true });\n    return () => {\n      gone = true;\n      img.removeEventListener('load', start);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [children, status]);\n\n  useEffect(() => {\n    wake();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [status, width, aspectRatio]);\n\n  useEffect(() => {\n    const s = sim.current;\n    return () => cancelAnimationFrame(s.raf);\n  }, []);\n\n  return (\n    <div\n      className={`refine-frame${className ? ` ${className}` : ''}`}\n      role=\"img\"\n      aria-label={text[status] ?? status}\n      aria-busy={active || undefined}\n      data-status={status}\n      data-active={active ? '' : undefined}\n      data-sweep={sweep && active ? '' : undefined}\n      data-mosaic={mosaic ? '' : undefined}\n      data-resolved={mosaic && resolved ? '' : undefined}\n      style={\n        {\n          '--rf-w': `${width}px`,\n          '--rf-aspect': aspectRatio,\n          '--rf-radius': `${radius}px`,\n          '--rf-bg': background,\n          '--rf-ink': color,\n          '--rf-stage': `${stageDuration}ms`,\n          '--rf-blur': `${mosaic ? 0 : stage.blur}px`,\n          '--rf-sat': stage.sat,\n          '--rf-scale': mosaic ? 1 : stage.scale,\n          '--rf-opacity': stage.opacity\n        } as CSSProperties\n      }\n    >\n      <div className=\"refine-frame__media\" aria-hidden=\"true\">\n        <div ref={printRef} className=\"refine-frame__print\">\n          {children}\n        </div>\n        <canvas ref={canvasRef} className=\"refine-frame__mosaic\" />\n      </div>\n      <div className=\"refine-frame__sweep\" aria-hidden=\"true\" />\n      {chip ? (\n        <div className=\"refine-frame__chip\" aria-hidden=\"true\">\n          <span className=\"refine-frame__mark\" data-kind={active ? 'spin' : status}>\n            {status === 'complete' ? (\n              <HugeiconsIcon icon={Tick02Icon} size={13} strokeWidth={2.5} />\n            ) : status === 'error' ? (\n              <HugeiconsIcon icon={Alert02Icon} size={13} strokeWidth={2.2} />\n            ) : (\n              <HugeiconsIcon icon={Loading03Icon} size={13} strokeWidth={2.2} />\n            )}\n          </span>\n          <span key={status} className=\"refine-frame__label\">\n            {text[status] ?? status}\n          </span>\n        </div>\n      ) : null}\n      {status === 'error' && onRetry ? (\n        <button type=\"button\" className=\"refine-frame__retry\" onClick={onRetry}>\n          <HugeiconsIcon icon={RefreshIcon} size={14} strokeWidth={2.2} />\n          <span>{retryLabel}</span>\n        </button>\n      ) : null}\n      <span className=\"refine-frame__sr\" role=\"status\">\n        {text[status] ?? status}\n      </span>\n    </div>\n  );\n};\n\nexport default RefineFrame;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}