{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "RefineFrame-JS-TW",
	"title": "RefineFrame",
	"description": "Media frame that steps an image through generation stages without any layout shift.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "RefineFrame/RefineFrame.jsx",
			"content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Alert02Icon, Loading03Icon, RefreshIcon, Tick02Icon } from '@hugeicons/core-free-icons';\nconst STAGES = {\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 = { 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 = {\n  queued: 'Queued',\n  generating: 'Generating',\n  refining: 'Refining',\n  complete: 'Ready',\n  error: 'Failed'\n};\nconst ACTIVE = new Set(['queued', 'generating', 'refining']);\n\nconst build = (s, canvas, img, dpr) => {\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 STYLE = `\n@keyframes refine-frame-sweep { from { background-position: 130% 0; } to { background-position: -130% 0; } }\n@keyframes refine-frame-spin { to { transform: rotate(360deg); } }\n@keyframes refine-frame-label { from { opacity: 0; filter: blur(2px); } }\n@keyframes refine-frame-wait { 0%, 100% { opacity: 0.45; } 50% { opacity: 0.65; } }\n`;\n\nexport default function RefineFrame({\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(null);\n  const canvasRef = useRef(null);\n  const sim = useRef({ p: 0, raf: 0, last: 0, key: '', w: 0, h: 0, levels: [], glint: null, sent: false });\n  const live = useRef({});\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(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 => {\n    const s = sim.current;\n    const c = live.current;\n    const canvas = canvasRef.current;\n    const img = printRef.current?.querySelector('img');\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');\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={`group relative isolate overflow-hidden text-[12px] leading-none font-medium [width:min(var(--rf-w),100%)] [aspect-ratio:var(--rf-aspect)] [border-radius:var(--rf-radius)] [background:var(--rf-bg)] [color:var(--rf-ink)] [font-family:inherit]${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        '--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      }}\n    >\n      <style>{STYLE}</style>\n      <div\n        className=\"absolute inset-0 [opacity:var(--rf-opacity)] [filter:blur(var(--rf-blur))_saturate(var(--rf-sat))] [transform:scale(var(--rf-scale))] [transition:opacity_var(--rf-stage)_cubic-bezier(0.23,1,0.32,1),filter_var(--rf-stage)_cubic-bezier(0.23,1,0.32,1),transform_var(--rf-stage)_cubic-bezier(0.23,1,0.32,1)] motion-reduce:[transition:opacity_var(--rf-stage)_ease] group-data-[status=queued]:[animation:refine-frame-wait_2.4s_ease-in-out_infinite] motion-reduce:[animation:none]!\"\n        aria-hidden=\"true\"\n      >\n        <div\n          ref={printRef}\n          className=\"h-full w-full [transition:opacity_var(--rf-stage)_ease] group-data-[mosaic]:opacity-0 group-data-[resolved]:opacity-100! [&>*]:block [&>*]:h-full [&>*]:w-full [&>*]:object-cover\"\n        >\n          {children}\n        </div>\n        <canvas\n          ref={canvasRef}\n          className=\"pointer-events-none absolute inset-0 h-full w-full opacity-0 [transition:opacity_var(--rf-stage)_ease] group-data-[mosaic]:opacity-100 group-data-[resolved]:opacity-0!\"\n        />\n      </div>\n      <div\n        className=\"pointer-events-none absolute inset-0 opacity-0 [background:linear-gradient(115deg,transparent_38%,color-mix(in_srgb,var(--rf-ink)_14%,transparent)_50%,transparent_62%)] [background-size:260%_100%] [transition:opacity_var(--rf-stage)_ease] group-data-[sweep]:opacity-100 group-data-[sweep]:[animation:refine-frame-sweep_2.2s_linear_infinite] group-data-[mosaic]:opacity-0! group-data-[mosaic]:[animation:none]! motion-reduce:group-data-[sweep]:opacity-0! motion-reduce:group-data-[sweep]:[animation:none]\"\n        aria-hidden=\"true\"\n      />\n      {chip ? (\n        <div\n          className=\"pointer-events-none absolute bottom-2.5 left-2.5 inline-flex h-[26px] translate-y-0 items-center gap-1.5 rounded-[13px] pr-2.5 pl-2 opacity-100 backdrop-blur-[8px] [background:color-mix(in_srgb,var(--rf-bg)_72%,transparent)] [transition:opacity_200ms_ease,transform_200ms_cubic-bezier(0.23,1,0.32,1)] starting:translate-y-1 starting:opacity-0 motion-reduce:[transition:opacity_200ms_ease]\"\n          aria-hidden=\"true\"\n        >\n          <span\n            className=\"inline-flex [color:color-mix(in_srgb,var(--rf-ink)_80%,transparent)] data-[kind=spin]:[animation:refine-frame-spin_1.1s_linear_infinite] data-[kind=error]:text-[#ef4444] motion-reduce:data-[kind=spin]:[animation:none]\"\n            data-kind={active ? 'spin' : status}\n          >\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=\"[animation:refine-frame-label_200ms_ease_both]\">\n            {text[status] ?? status}\n          </span>\n        </div>\n      ) : null}\n      {status === 'error' && onRetry ? (\n        <button\n          type=\"button\"\n          className=\"absolute top-1/2 left-1/2 inline-flex h-8 -translate-x-1/2 -translate-y-1/2 cursor-pointer items-center gap-1.5 rounded-2xl border-0 pr-3.5 pl-3 text-[13px] font-medium opacity-100 outline-none [background:color-mix(in_srgb,var(--rf-ink)_14%,var(--rf-bg))] [color:var(--rf-ink)] [font-family:inherit] [-webkit-tap-highlight-color:transparent] [transition:opacity_200ms_ease,transform_160ms_cubic-bezier(0.23,1,0.32,1),background-color_150ms_ease] starting:scale-[0.96] starting:opacity-0 active:scale-[0.96] [@media(hover:hover)_and_(pointer:fine)]:hover:[background:color-mix(in_srgb,var(--rf-ink)_20%,var(--rf-bg))] motion-reduce:[transition:opacity_200ms_ease] motion-reduce:active:scale-100\"\n          onClick={onRetry}\n        >\n          <HugeiconsIcon icon={RefreshIcon} size={14} strokeWidth={2.2} />\n          <span>{retryLabel}</span>\n        </button>\n      ) : null}\n      <span className=\"sr-only\" role=\"status\">\n        {text[status] ?? status}\n      </span>\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}