{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-edit-review",
  "title": "AI Edit Review",
  "description": "An AI text editor that rewrites a selected passage, streams a word-level diff review with accept/reject actions, and morphs a dock through writing, review, and applied states.",
  "dependencies": [
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/ai-edit-review/ai-edit-review.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\ntype RGB = [number, number, number];\ntype Flow = \"idle\" | \"working\" | \"review\" | \"applied\";\ntype Status = \"auto\" | Flow;\ntype EditAction = { label: string; result: string };\ntype Op = { kind: \"eq\" | \"del\" | \"ins\"; text: string };\n\nexport interface AIEditReviewProps {\n  status?: Status;\n  interactive?: boolean;\n  loop?: boolean;\n  holdSeconds?: number;\n  workSeconds?: number;\n  reviewSeconds?: number;\n  before?: string;\n  selected?: string;\n  after?: string;\n  actions?: EditAction[];\n  workingText?: string;\n  reviewText?: string;\n  appliedText?: string;\n  acceptLabel?: string;\n  rejectLabel?: string;\n  description?: string;\n  font?: React.CSSProperties;\n  textColor?: string;\n  mutedColor?: string;\n  selectionColor?: string;\n  addColor?: string;\n  removeColor?: string;\n  workColors?: string[];\n  reviewColors?: string[];\n  doneColors?: string[];\n  orbColor?: string;\n  particles?: number;\n  orbAura?: number;\n  ringIntensity?: number;\n  ringSize?: number;\n  ringSpeed?: number;\n  background?: string;\n  borderColor?: string;\n  radius?: number;\n  padding?: number;\n  gap?: number;\n  onStart?: () => void;\n  onAccept?: () => void;\n  onReject?: () => void;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nconst TAU = Math.PI * 2;\nconst BANDS = 32;\nconst LEAD = 36;\nconst DOCK_PAD = 8;\nconst DOCK_H = LEAD + DOCK_PAD * 2;\nconst DOCK_GAP = 8;\nconst ORB_SIDE = 150;\nconst ORB_RATIO = 0.6;\nconst AURA_GAIN = 0.3;\nconst WORD_MS = 60;\nconst MAX_WORDS = 400;\nconst START_ANGLE = 40;\nconst STATIC_SHIMMER = 72;\nconst FLOWS: Flow[] = [\"idle\", \"working\", \"review\", \"applied\"];\nconst MORPH = \"0.55s cubic-bezier(0.3, 1.2, 0.4, 1)\";\nconst FONT_FALLBACK = \"Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif\";\nconst HAIRLINE = \"rgba(255,255,255,0.14)\";\nconst HOVER_ON = \"inset 0 0 0 999px rgba(255,255,255,0.07)\";\nconst HOVER_OFF = \"inset 0 0 0 999px rgba(255,255,255,0)\";\nconst RING = \"0 0 0 2px rgba(255,255,255,0.35)\";\nconst NO_RING = \"0 0 0 0 rgba(255,255,255,0)\";\nconst SHEEN =\n  \"linear-gradient(115deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0.02) 30%, rgba(255,255,255,0) 48%, rgba(255,255,255,0.04) 74%, rgba(255,255,255,0) 100%), linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0) 60%)\";\n\nconst DEFAULT_BEFORE = \"Every team has a story hidden in its numbers. \";\nconst DEFAULT_SELECTED =\n  \"We made a new dashboard that is really fast and shows you all of the things you need to know about how your team is doing right now.\";\nconst DEFAULT_AFTER = \" Share any view with one click.\";\nconst DEFAULT_ACTIONS: EditAction[] = [\n  {\n    label: \"Improve\",\n    result: \"Our new dashboard is built for speed and shows you exactly how your team is doing, right now.\",\n  },\n  {\n    label: \"Shorten\",\n    result: \"A fast dashboard that shows how your team is doing.\",\n  },\n  {\n    label: \"Translate\",\n    result: \"Yeni panelimiz hızlıdır ve ekibinizin şu anki durumunu bir bakışta gösterir.\",\n  },\n];\nconst DEFAULT_WORK = [\"#FF7BD5\", \"#FFB86B\", \"#F5C84C\"];\nconst DEFAULT_REVIEW = [\"#7CF3FF\", \"#6EA8FF\", \"#9B7BFF\"];\nconst DEFAULT_DONE = [\"#7CFFC4\", \"#7CF3FF\", \"#F5F6F8\"];\nconst DEFAULT_DESCRIPTION =\n  \"AI text editor that rewrites a selected passage, shows a word-level review, and lets you accept or reject the change.\";\n\nconst SR_ONLY: React.CSSProperties = {\n  position: \"absolute\",\n  width: 1,\n  height: 1,\n  overflow: \"hidden\",\n  clip: \"rect(0 0 0 0)\",\n  whiteSpace: \"nowrap\",\n};\n\ntype Sizes = Record<Flow, number>;\n\ntype Live = {\n  flow: Flow;\n  level: number;\n  bands: Float32Array;\n  target: Float32Array;\n  time: number;\n  phase: number;\n  rot: number;\n  angle: number;\n  ringSpeed: number;\n  aura: number;\n  base: RGB;\n  accent: RGB;\n  accents: Record<Flow, RGB>;\n  vis: VisParams;\n  still: boolean;\n  drawnFlow: Flow | null;\n};\n\nfunction clamp01(v: number) {\n  return Math.min(1, Math.max(0, v));\n}\n\nfunction parseRgb(input: string, fallback: RGB): RGB {\n  const s = (input || \"\").trim();\n  if (s.startsWith(\"#\")) {\n    let h = s.slice(1);\n    if (h.length === 3 || h.length === 4)\n      h = h\n        .split(\"\")\n        .map((c) => c + c)\n        .join(\"\");\n    const n = parseInt(h.slice(0, 6), 16);\n    if (h.length >= 6 && !Number.isNaN(n)) return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n  }\n  const m = s.match(/rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/i);\n  if (m) return [Math.round(+m[1]), Math.round(+m[2]), Math.round(+m[3])];\n  return fallback;\n}\n\nfunction mixRgb(a: RGB, b: RGB, t: number): RGB {\n  return [Math.round(a[0] + (b[0] - a[0]) * t), Math.round(a[1] + (b[1] - a[1]) * t), Math.round(a[2] + (b[2] - a[2]) * t)];\n}\n\nfunction rgba(c: RGB, a: number) {\n  return `rgba(${c[0]},${c[1]},${c[2]},${a})`;\n}\n\nfunction cleanList(list: string[] | undefined, fallback: string[]) {\n  const out = (list ?? []).filter((s): s is string => typeof s === \"string\" && s.trim() !== \"\");\n  return out.length ? out : fallback;\n}\n\nfunction cleanActions(list: EditAction[] | undefined): EditAction[] {\n  const out = (list ?? []).filter(\n    (a): a is EditAction => !!a && typeof a.label === \"string\" && a.label.trim() !== \"\" && typeof a.result === \"string\",\n  );\n  return out.length ? out.slice(0, 5) : DEFAULT_ACTIONS;\n}\n\nfunction buildGradient(colors: string[], spread: number) {\n  const arc = Math.max(0.15, Math.min(1, spread)) * 360;\n  const n = colors.length;\n  if (n === 0) return \"linear-gradient(transparent, transparent)\";\n  const stops = colors.map((c, i) => {\n    const at = n === 1 ? 0.5 : 0.1 + (0.8 * i) / (n - 1);\n    return `${c} ${(at * arc).toFixed(1)}deg`;\n  });\n  return `conic-gradient(from var(--gl-angle, ${START_ANGLE}deg), transparent 0deg, ${stops.join(\", \")}, transparent ${arc.toFixed(1)}deg, transparent 360deg)`;\n}\n\nfunction ringStyle(width: number): React.CSSProperties {\n  return {\n    position: \"absolute\",\n    inset: 0,\n    boxSizing: \"border-box\",\n    borderRadius: \"inherit\",\n    padding: width,\n    WebkitMask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n    WebkitMaskComposite: \"xor\",\n    mask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n    maskComposite: \"exclude\",\n  };\n}\n\nfunction diffWords(a: string, b: string): Op[] {\n  const A = a.split(/\\s+/).filter(Boolean);\n  const B = b.split(/\\s+/).filter(Boolean);\n  if (A.length > MAX_WORDS || B.length > MAX_WORDS)\n    return [...A.map((t) => ({ kind: \"del\" as const, text: t })), ...B.map((t) => ({ kind: \"ins\" as const, text: t }))];\n  const n = A.length;\n  const m = B.length;\n  const dp: Uint16Array[] = [];\n  for (let i = 0; i <= n; i++) dp.push(new Uint16Array(m + 1));\n  for (let i = n - 1; i >= 0; i--)\n    for (let j = m - 1; j >= 0; j--) dp[i][j] = A[i] === B[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);\n  const out: Op[] = [];\n  let i = 0;\n  let j = 0;\n  while (i < n && j < m) {\n    if (A[i] === B[j]) {\n      out.push({ kind: \"eq\", text: A[i] });\n      i++;\n      j++;\n    } else if (dp[i + 1][j] >= dp[i][j + 1]) {\n      out.push({ kind: \"del\", text: A[i++] });\n    } else {\n      out.push({ kind: \"ins\", text: B[j++] });\n    }\n  }\n  while (i < n) out.push({ kind: \"del\", text: A[i++] });\n  while (j < m) out.push({ kind: \"ins\", text: B[j++] });\n  return out;\n}\n\ntype Sim = \"idle\" | \"listening\" | \"thinking\" | \"speaking\";\n\nfunction simulateBands(t: number, sim: Sim, out: Float32Array) {\n  const n = out.length;\n  let base = 0;\n  if (sim === \"idle\") {\n    base = 0.02 + 0.015 * Math.sin(t * 1.3);\n  } else if (sim === \"listening\") {\n    const x = 0.5 + 0.5 * Math.sin(t * 0.9 + 1.7 * Math.sin(t * 0.37));\n    const gate = clamp01((x - 0.35) / 0.3);\n    const syll = 0.55 + 0.45 * Math.sin(t * 7.3) * Math.sin(t * 2.1 + 0.6);\n    base = gate * gate * (3 - 2 * gate) * syll;\n  } else if (sim === \"thinking\") {\n    base = 0.16 + 0.08 * Math.sin(t * 1.2) + 0.05 * Math.sin(t * 2.9 + 1);\n  } else {\n    const syll = 0.6 + 0.4 * Math.sin(t * 5.2) * Math.sin(t * 1.6);\n    base = (0.85 + 0.15 * Math.sin(t * 1.1)) * syll;\n  }\n  for (let i = 0; i < n; i++) {\n    const f = i / (n - 1);\n    const shape = 0.25 + 0.75 * Math.exp(-Math.pow((f - 0.28) / 0.3, 2));\n    const v = 0.5 + 0.5 * Math.sin(t * (3.1 + i * 0.53) + i * 1.9) * Math.sin(t * (1.3 + i * 0.21) + i);\n    out[i] = clamp01(base * shape * (0.35 + 0.65 * v) * 0.9);\n  }\n}\n\nfunction simOf(flow: Flow): Sim {\n  return flow === \"working\" ? \"speaking\" : flow === \"review\" ? \"thinking\" : \"idle\";\n}\n\nfunction updateAudio(L: Live, dt: number) {\n  const n = L.bands.length;\n  simulateBands(L.time, simOf(L.flow), L.target);\n  let sum = 0;\n  for (let i = 0; i < n; i++) {\n    const tv = L.target[i];\n    const cur = L.bands[i];\n    const k = tv > cur ? Math.min(1, dt * 22) : Math.min(1, dt * 6);\n    L.bands[i] = cur + (tv - cur) * k;\n    sum += L.bands[i];\n  }\n  const lv = clamp01((sum / n) * 2.4);\n  L.level += (lv - L.level) * Math.min(1, dt * 14);\n  L.phase += dt * (1 + 1.6 * L.level);\n  L.rot += dt * 0.45 * (1 + L.level);\n  L.accent = mixRgb(L.accent, L.accents[L.flow], Math.min(1, dt * 5));\n}\n\ntype Particle = { x: number; y: number; z: number; size: number; phase: number };\ntype VisParams = { pts: Particle[]; rgb: RGB };\n\nconst STRIDE = 5;\nconst MAX_PARTICLES = 1000;\nconst TILT = 0.3;\nconst ORB_OUT = new Float32Array(MAX_PARTICLES * STRIDE);\nconst LX = -0.34;\nconst LY = -0.58;\nconst LZ = 0.74;\n\nfunction rng(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction buildParticles(count: number): Particle[] {\n  const n = Math.max(40, Math.min(MAX_PARTICLES, Math.round(count)));\n  const rnd = rng(97);\n  const golden = Math.PI * (3 - Math.sqrt(5));\n  const out: Particle[] = [];\n  for (let i = 0; i < n; i++) {\n    const y = 1 - (2 * (i + 0.5)) / n;\n    const rr = Math.sqrt(Math.max(0, 1 - y * y));\n    const a = i * golden;\n    out.push({\n      x: rr * Math.cos(a),\n      y,\n      z: rr * Math.sin(a),\n      size: rnd(),\n      phase: rnd(),\n    });\n  }\n  return out;\n}\n\nfunction layoutParticles(pts: Particle[], t: number, rot: number, level: number, out: Float32Array) {\n  const yaw = rot * 0.6;\n  const cy = Math.cos(yaw);\n  const sy = Math.sin(yaw);\n  const ct = Math.cos(TILT);\n  const st = Math.sin(TILT);\n  const amp = 0.4 + 1.0 * level;\n  const base = 0.72 + 0.12 * level;\n  const dotScale = Math.min(1.1, Math.sqrt(430 / Math.max(1, pts.length)));\n  for (let i = 0; i < pts.length; i++) {\n    const p = pts[i];\n    const a1 = 1.8 * p.x + 1.4 * p.y + t * 1.0;\n    const a2 = 2.5 * p.y - 1.8 * p.z - t * 1.3;\n    const a3 = 3.2 * p.z + 1.2 * p.x + t * 1.7;\n    const wr = 1 + amp * (0.11 * Math.sin(a1) + 0.08 * Math.sin(a2) + 0.06 * Math.sin(a3));\n    const c1 = amp * 0.11 * Math.cos(a1);\n    const c2 = amp * 0.08 * Math.cos(a2);\n    const c3 = amp * 0.06 * Math.cos(a3);\n    const gx = 1.8 * c1 + 1.2 * c3;\n    const gy = 1.4 * c1 + 2.5 * c2;\n    const gz = -1.8 * c2 + 3.2 * c3;\n    const gd = gx * p.x + gy * p.y + gz * p.z;\n    const bnx = p.x - (gx - gd * p.x) / wr;\n    const bny = p.y - (gy - gd * p.y) / wr;\n    const bnz = p.z - (gz - gd * p.z) / wr;\n    const bnl = Math.hypot(bnx, bny, bnz) || 1;\n    const grow = wr * base;\n    const lx = p.x * grow;\n    const ly = p.y * grow;\n    const lz = p.z * grow;\n    const x = lx * cy + lz * sy;\n    let z = -lx * sy + lz * cy;\n    let y = ly;\n    const nx = (bnx / bnl) * cy + (bnz / bnl) * sy;\n    let nz = -(bnx / bnl) * sy + (bnz / bnl) * cy;\n    let ny = bny / bnl;\n    let a = y * ct - z * st;\n    z = y * st + z * ct;\n    y = a;\n    a = ny * ct - nz * st;\n    nz = ny * st + nz * ct;\n    ny = a;\n\n    const persp = 1 / (1 - z * 0.28);\n    const d = clamp01((z + 1) / 2);\n    const mod = clamp01(0.5 + (wr - 1) / (amp * 0.5));\n    const size = 0.35 + 0.65 * mod;\n    const lam = Math.max(0, nx * LX + ny * LY + nz * LZ);\n    const light = 0.22 + 0.78 * Math.pow(lam, 0.9);\n    const rim = nz > 0 ? Math.pow(1 - nz, 3) : 0;\n    const tw = 0.9 + 0.1 * Math.sin(t * (1 + p.size * 2) + p.phase * 40);\n    const o = i * STRIDE;\n    out[o] = x * persp;\n    out[o + 1] = y * persp;\n    out[o + 2] = (0.95 + 0.25 * p.size) * (0.4 + 0.6 * size) * (0.55 + 0.6 * d) * dotScale * (1 + 0.35 * level);\n    out[o + 3] = Math.min(1, light * (0.3 + 0.7 * Math.pow(d, 1.1)) * (0.45 + 0.55 * size) * tw * 1.15 + rim * 0.28 * (0.4 + 0.6 * d));\n    out[o + 4] = Math.min(1, rim * 0.9 + level * 0.55);\n  }\n}\n\nfunction visDraw(ctx: CanvasRenderingContext2D, w: number, h: number, L: Live) {\n  const side = Math.min(w, h);\n  const pts = L.vis.pts;\n  layoutParticles(pts, L.phase, L.rot, L.level, ORB_OUT);\n  const cx = w / 2;\n  const cy = h / 2;\n  const R = side * 0.38;\n  const k = side / 64;\n  for (let i = 0; i < pts.length; i++) {\n    const o = i * STRIDE;\n    const a = ORB_OUT[o + 3];\n    const r = ORB_OUT[o + 2] * k;\n    if (a < 0.02 || r < 0.15) continue;\n    const [cr, cg, cb] = mixRgb(L.vis.rgb, L.accent, ORB_OUT[o + 4] * 0.7);\n    ctx.fillStyle = `rgba(${cr},${cg},${cb},${a.toFixed(3)})`;\n    ctx.beginPath();\n    ctx.arc(cx + ORB_OUT[o] * R, cy + ORB_OUT[o + 1] * R, r, 0, TAU);\n    ctx.fill();\n  }\n}\n\nfunction useFocusVisible() {\n  const [focusVisible, setFocusVisible] = React.useState(false);\n  return {\n    focusVisible,\n    onFocus: (e: React.FocusEvent<HTMLElement>) => {\n      try {\n        setFocusVisible(e.currentTarget.matches(\":focus-visible\"));\n      } catch {\n        setFocusVisible(true);\n      }\n    },\n    onBlur: () => setFocusVisible(false),\n  };\n}\n\nfunction subscribeReducedMotion(callback: () => void) {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return () => undefined;\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  if (typeof mq.addEventListener !== \"function\") return () => undefined;\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\nfunction getReducedMotionSnapshot() {\n  return typeof window !== \"undefined\" && typeof window.matchMedia === \"function\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\nfunction getReducedMotionServerSnapshot() {\n  return false;\n}\n\nfunction Icon({ size = 16, children }: { size?: number; children: React.ReactNode }) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n      style={{ display: \"block\" }}\n    >\n      {children}\n    </svg>\n  );\n}\n\nconst CheckIcon = () => (\n  <Icon size={15}>\n    <path d=\"M20 6 9 17l-5-5\" />\n  </Icon>\n);\nconst CloseIcon = () => (\n  <Icon size={14}>\n    <path d=\"M18 6 6 18\" />\n    <path d=\"m6 6 12 12\" />\n  </Icon>\n);\nconst StopIcon = () => (\n  <svg width={20} height={20} viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\" style={{ display: \"block\" }}>\n    <rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"4\" />\n  </svg>\n);\n\ntype DockButtonProps = {\n  label: string;\n  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;\n  live: boolean;\n  variant: \"ghost\" | \"soft\" | \"primary\" | \"round\";\n  textStyle: React.CSSProperties;\n  textColor: string;\n  primary?: boolean;\n  icon?: React.ReactNode;\n  ariaLabel?: string;\n  children?: React.ReactNode;\n};\n\nfunction DockButton({ label, onClick, live, variant, textStyle, textColor, primary, icon, ariaLabel }: DockButtonProps) {\n  const [hover, setHover] = React.useState(false);\n  const [pressed, setPressed] = React.useState(false);\n  const focus = useFocusVisible();\n  const release = () => setPressed(false);\n  const round = variant === \"round\";\n  const bg = variant === \"primary\" || round ? textColor : variant === \"soft\" ? \"rgba(255,255,255,0.08)\" : \"rgba(255,255,255,0.045)\";\n  const fg = variant === \"primary\" || round ? \"#0E0E10\" : textColor;\n  return (\n    <button\n      type=\"button\"\n      aria-label={ariaLabel ?? (round ? label : undefined)}\n      data-primary={primary ? \"true\" : undefined}\n      disabled={!live}\n      tabIndex={live ? 0 : -1}\n      onClick={onClick}\n      onMouseEnter={() => setHover(true)}\n      onMouseLeave={() => setHover(false)}\n      onPointerDown={() => setPressed(true)}\n      onPointerUp={release}\n      onPointerLeave={release}\n      onPointerCancel={release}\n      onFocus={focus.onFocus}\n      onBlur={focus.onBlur}\n      style={{\n        ...textStyle,\n        flex: \"none\",\n        height: LEAD,\n        width: round ? LEAD : undefined,\n        margin: 0,\n        padding: round ? 0 : icon ? \"0 16px 0 12px\" : \"0 16px\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        gap: 6,\n        borderRadius: 999,\n        border: `1px solid ${HAIRLINE}`,\n        background: bg,\n        color: fg,\n        fontWeight: variant === \"primary\" ? 600 : 500,\n        whiteSpace: \"nowrap\",\n        cursor: live ? \"pointer\" : \"default\",\n        outline: \"none\",\n        transform: pressed && live ? \"scale(0.95)\" : \"scale(1)\",\n        boxShadow: `${hover && live ? HOVER_ON : HOVER_OFF}, ${focus.focusVisible ? RING : NO_RING}`,\n        transition: \"background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease\",\n      }}\n    >\n      {icon}\n      {!round && label}\n    </button>\n  );\n}\n\nfunction estimate(text: string, px: number) {\n  return Math.ceil(text.length * px * 0.56);\n}\n\ntype DockActionProps = {\n  label: string;\n  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;\n  live: boolean;\n  variant: DockButtonProps[\"variant\"];\n  textStyle: React.CSSProperties;\n  textColor: string;\n  extra?: Partial<DockButtonProps>;\n};\n\nfunction DockAction({ label, onClick, live, variant, textStyle, textColor, extra }: DockActionProps) {\n  return <DockButton label={label} onClick={onClick} live={live} variant={variant} textStyle={textStyle} textColor={textColor} {...extra} />;\n}\n\nexport function AIEditReview(props: AIEditReviewProps) {\n  const {\n    status = \"auto\",\n    interactive = true,\n    loop = true,\n    holdSeconds = 2.2,\n    workSeconds = 2,\n    reviewSeconds = 2.6,\n    before = DEFAULT_BEFORE,\n    selected = DEFAULT_SELECTED,\n    after = DEFAULT_AFTER,\n    actions = DEFAULT_ACTIONS,\n    workingText = \"Rewriting\",\n    reviewText = \"Review\",\n    appliedText = \"Applied\",\n    acceptLabel = \"Accept\",\n    rejectLabel = \"Reject\",\n    description = DEFAULT_DESCRIPTION,\n    font = { fontSize: 17, lineHeight: 1.6 },\n    textColor = \"#EDEDED\",\n    mutedColor = \"#6B6B6B\",\n    selectionColor = \"#9B7BFF\",\n    addColor = \"#7CFFC4\",\n    removeColor = \"#FF7B8A\",\n    workColors = DEFAULT_WORK,\n    reviewColors = DEFAULT_REVIEW,\n    doneColors = DEFAULT_DONE,\n    orbColor = \"#FFFFFF\",\n    particles = 520,\n    orbAura = 0.5,\n    ringIntensity = 0.7,\n    ringSize = 10,\n    ringSpeed = 40,\n    background = \"#0E0E10\",\n    borderColor = \"rgba(255,255,255,0.09)\",\n    radius = 28,\n    padding = 28,\n    gap = 22,\n    onStart,\n    onAccept,\n    onReject,\n    className,\n    style,\n  } = props;\n\n  const rootRef = React.useRef<HTMLElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const haloRef = React.useRef<HTMLDivElement>(null);\n  const groupEls = React.useRef<Record<string, HTMLDivElement | null>>({});\n  const delEls = React.useRef<(HTMLSpanElement | null)[]>([]);\n  const textEls = React.useRef<(HTMLSpanElement | null)[]>([]);\n  const ringEls = React.useRef<(HTMLDivElement | null)[]>([]);\n  const isVisibleRef = React.useRef(true);\n  const prevFlow = React.useRef<Flow>(\"idle\");\n  const kbdRef = React.useRef(false);\n  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n  const streamRef = React.useRef<ReturnType<typeof setInterval> | null>(null);\n  const rafRef = React.useRef(0);\n\n  const reduce = React.useSyncExternalStore(subscribeReducedMotion, getReducedMotionSnapshot, getReducedMotionServerSnapshot);\n\n  const actionList = React.useMemo(() => cleanActions(actions), [actions]);\n  const workList = React.useMemo(() => cleanList(workColors, DEFAULT_WORK), [workColors]);\n  const reviewList = React.useMemo(() => cleanList(reviewColors, DEFAULT_REVIEW), [reviewColors]);\n  const doneList = React.useMemo(() => cleanList(doneColors, DEFAULT_DONE), [doneColors]);\n  const workGradient = React.useMemo(() => buildGradient(workList, 0.55), [workList]);\n  const reviewGradient = React.useMemo(() => buildGradient(reviewList, 0.55), [reviewList]);\n  const doneGradient = React.useMemo(() => buildGradient(doneList, 0.55), [doneList]);\n  const workRgb = React.useMemo(() => parseRgb(workList[0], [255, 123, 213]), [workList]);\n  const reviewRgb = React.useMemo(() => parseRgb(reviewList[0], [124, 243, 255]), [reviewList]);\n  const doneRgb = React.useMemo(() => parseRgb(doneList[0], [124, 255, 196]), [doneList]);\n  const selRgb = React.useMemo(() => parseRgb(selectionColor, [155, 123, 255]), [selectionColor]);\n  const addRgb = React.useMemo(() => parseRgb(addColor, [124, 255, 196]), [addColor]);\n  const delRgb = React.useMemo(() => parseRgb(removeColor, [255, 123, 138]), [removeColor]);\n  const pts = React.useMemo(() => buildParticles(particles), [particles]);\n  const visBase = React.useMemo(() => parseRgb(orbColor, [255, 255, 255]), [orbColor]);\n  const visParams = React.useMemo<VisParams>(() => ({ pts, rgb: visBase }), [pts, visBase]);\n  const accents: Record<Flow, RGB> = {\n    idle: reviewRgb,\n    working: workRgb,\n    review: reviewRgb,\n    applied: doneRgb,\n  };\n\n  const [flow, setFlow] = React.useState<Flow>(status === \"auto\" ? \"idle\" : status);\n  const [run, setRun] = React.useState(status === \"auto\");\n  const [manual, setManual] = React.useState(false);\n  const [active, setActive] = React.useState(0);\n  const [streamed, setStreamed] = React.useState(0);\n  const [delW, setDelW] = React.useState<number[]>([]);\n\n  const [prevStatus, setPrevStatus] = React.useState(status);\n  if (prevStatus !== status) {\n    setPrevStatus(status);\n    setFlow(status === \"auto\" ? \"idle\" : status);\n    setRun(status === \"auto\");\n    setManual(false);\n    setActive(0);\n  }\n\n  const shownFlow: Flow = flow;\n  const current = actionList[Math.min(active, actionList.length - 1)];\n  const ops = React.useMemo(() => diffWords(selected, current.result), [selected, current.result]);\n  const insCount = ops.filter((o) => o.kind === \"ins\").length;\n  const delCount = ops.filter((o) => o.kind === \"del\").length;\n  const reviewing = shownFlow === \"review\" || shownFlow === \"applied\";\n  const shownIns = shownFlow === \"applied\" ? insCount : streamed;\n\n  const [prevReviewKey, setPrevReviewKey] = React.useState<{ flow: Flow; ops: Op[] }>({ flow, ops });\n  if (prevReviewKey.flow !== flow || prevReviewKey.ops !== ops) {\n    setPrevReviewKey({ flow, ops });\n    setStreamed(0);\n  }\n\n  const dockPx = 14;\n  const estSizes = (): Sizes => ({\n    idle: actionList.reduce((s, a) => s + estimate(a.label, dockPx) + 34, 0) + (actionList.length - 1) * 6,\n    working: estimate(workingText, dockPx) + 10 + LEAD,\n    review:\n      estimate(`+${insCount}`, dockPx) +\n      8 +\n      estimate(`−${delCount}`, dockPx) +\n      4 +\n      6 +\n      (estimate(rejectLabel, dockPx) + 30 + 14 + 6) +\n      6 +\n      (estimate(acceptLabel, dockPx) + 30 + 15 + 6),\n    applied: estimate(appliedText, dockPx) + 4,\n  });\n  const [sizes, setSizes] = React.useState<Sizes>(estSizes);\n\n  const liveRef = React.useRef<Live>({\n    flow: shownFlow,\n    level: 0,\n    bands: new Float32Array(BANDS),\n    target: new Float32Array(BANDS),\n    time: 0,\n    phase: 0,\n    rot: 0,\n    angle: START_ANGLE,\n    ringSpeed,\n    aura: orbAura * AURA_GAIN,\n    base: visBase,\n    accent: accents[shownFlow],\n    accents,\n    vis: visParams,\n    still: false,\n    drawnFlow: null,\n  });\n  React.useLayoutEffect(() => {\n    const live = liveRef.current;\n    live.flow = shownFlow;\n    live.ringSpeed = ringSpeed;\n    live.aura = orbAura * AURA_GAIN;\n    live.base = visBase;\n    live.accents = accents;\n    live.vis = visParams;\n    live.still = reduce;\n  });\n\n  React.useEffect(() => {\n    if (!run) return;\n    let ms = 0;\n    let go: (() => void) | null = null;\n    if (flow === \"idle\") {\n      ms = holdSeconds * 1000;\n      go = () => setFlow(\"working\");\n    } else if (flow === \"working\") {\n      ms = workSeconds * 1000;\n      go = () => setFlow(\"review\");\n    } else if (flow === \"review\" && !manual) {\n      ms = insCount * WORD_MS + reviewSeconds * 1000;\n      go = () => {\n        onAccept?.();\n        setFlow(\"applied\");\n      };\n    } else if (flow === \"applied\") {\n      ms = holdSeconds * 1000 * 0.8;\n      go = () => {\n        setFlow(\"idle\");\n        setManual(false);\n        setActive((a) => (a + 1) % actionList.length);\n        if (!(status === \"auto\" && loop)) setRun(false);\n      };\n    }\n    if (!go) return;\n    const fn = go;\n    if (timerRef.current) clearTimeout(timerRef.current);\n    timerRef.current = setTimeout(fn, Math.max(400, ms));\n    return () => {\n      if (timerRef.current) clearTimeout(timerRef.current);\n    };\n  }, [run, flow, manual, holdSeconds, workSeconds, reviewSeconds, insCount, status, loop, actionList.length, onAccept]);\n\n  React.useEffect(() => {\n    const prev = prevFlow.current;\n    prevFlow.current = flow;\n    if (prev === \"idle\" && flow === \"working\") onStart?.();\n  }, [flow, onStart]);\n\n  React.useEffect(() => {\n    if (!kbdRef.current) return;\n    const el = groupEls.current[flow];\n    const target = el ? (el.querySelector('[data-primary=\"true\"]:not([disabled])') as HTMLElement | null) : null;\n    if (target) target.focus();\n  }, [flow]);\n\n  React.useEffect(() => {\n    if (flow !== \"review\") return;\n    let n = 0;\n    if (streamRef.current) clearInterval(streamRef.current);\n    streamRef.current = setInterval(() => {\n      if (!isVisibleRef.current || n >= insCount) return;\n      n += 1;\n      setStreamed(n);\n    }, WORD_MS);\n    return () => {\n      if (streamRef.current) clearInterval(streamRef.current);\n    };\n  }, [flow, ops, insCount]);\n\n  React.useEffect(() => {\n    if (flow !== \"review\") return;\n    setDelW(delEls.current.map((el) => (el ? el.offsetWidth : 0)));\n  }, [flow, ops]);\n\n  React.useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        isVisibleRef.current = entry.isIntersecting;\n      },\n      { threshold: 0 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  const measureKey = `${actionList.map((a) => a.label).join(\"|\")}|${workingText}|${reviewText}|${appliedText}|${acceptLabel}|${rejectLabel}|${insCount}|${delCount}`;\n  const fontKey = JSON.stringify(font);\n  React.useEffect(() => {\n    const w = (k: Flow) => {\n      const node = groupEls.current[k];\n      return node ? node.offsetWidth : 0;\n    };\n    const measure = () => {\n      const next: Sizes = {\n        idle: w(\"idle\"),\n        working: w(\"working\"),\n        review: w(\"review\"),\n        applied: w(\"applied\"),\n      };\n      setSizes((prev) => (FLOWS.every((f) => prev[f] === next[f]) ? prev : next));\n    };\n    measure();\n    let ro: ResizeObserver | null = null;\n    if (typeof ResizeObserver === \"function\") {\n      ro = new ResizeObserver(measure);\n      for (const f of FLOWS) {\n        const node = groupEls.current[f];\n        if (node) ro.observe(node);\n      }\n    }\n    return () => {\n      if (ro) ro.disconnect();\n    };\n  }, [measureKey, fontKey]);\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas ? canvas.getContext(\"2d\") : null;\n    if (!root) return;\n\n    let last = performance.now();\n\n    const fit = () => {\n      if (!canvas || !ctx) return;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.round(ORB_SIDE * dpr);\n      canvas.height = Math.round(ORB_SIDE * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    };\n    const draw = () => {\n      if (!ctx) return;\n      ctx.clearRect(0, 0, ORB_SIDE, ORB_SIDE);\n      visDraw(ctx, ORB_SIDE, ORB_SIDE, liveRef.current);\n    };\n    const updateDom = (L: Live) => {\n      const t = L.time;\n      const pos = (100 - 100 * ((t / 2.4) % 1)).toFixed(2);\n      for (const node of textEls.current) if (node) node.style.backgroundPosition = `${pos}% 50%`;\n      const ringOpacity = (0.3 + 0.7 * L.level).toFixed(3);\n      for (const node of ringEls.current) if (node) node.style.opacity = ringOpacity;\n      const halo = haloRef.current;\n      if (halo) {\n        halo.style.opacity = (L.aura * (0.2 + 0.8 * L.level)).toFixed(3);\n        halo.style.transform = `scale(${(0.9 + 0.3 * L.level).toFixed(3)})`;\n      }\n    };\n\n    fit();\n    if (reduce) root.style.setProperty(\"--gl-angle\", `${START_ANGLE}deg`);\n\n    const frame = (now: number) => {\n      const dt = Math.min((now - last) / 1000, 0.05);\n      last = now;\n      if (isVisibleRef.current) {\n        const L = liveRef.current;\n        if (L.still) {\n          if (L.drawnFlow !== L.flow) {\n            L.time = 0;\n            L.phase = 0;\n            L.rot = 0;\n            L.level = L.flow === \"working\" ? 0.4 : 0.05;\n            L.accent = L.accents[L.flow];\n            draw();\n            updateDom(L);\n            L.drawnFlow = L.flow;\n          }\n        } else {\n          L.time += dt;\n          L.angle = (L.angle + L.ringSpeed * (L.flow === \"idle\" ? 0.5 : 1) * dt) % 360;\n          updateAudio(L, dt);\n          root.style.setProperty(\"--gl-angle\", `${L.angle.toFixed(2)}deg`);\n          draw();\n          updateDom(L);\n          L.drawnFlow = L.flow;\n        }\n      }\n      rafRef.current = requestAnimationFrame(frame);\n    };\n    rafRef.current = requestAnimationFrame(frame);\n\n    return () => cancelAnimationFrame(rafRef.current);\n  }, [reduce]);\n\n  const fromKeyboard = (e: React.MouseEvent<HTMLButtonElement>) => {\n    kbdRef.current = e.detail === 0;\n  };\n  const handleAction = (i: number) => (e: React.MouseEvent<HTMLButtonElement>) => {\n    if (!interactive) return;\n    fromKeyboard(e);\n    setActive(i);\n    setManual(true);\n    setRun(true);\n    setFlow(\"working\");\n  };\n  const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {\n    if (!interactive) return;\n    fromKeyboard(e);\n    setManual(false);\n    setRun(false);\n    setFlow(\"idle\");\n  };\n  const handleAccept = (e: React.MouseEvent<HTMLButtonElement>) => {\n    if (!interactive) return;\n    fromKeyboard(e);\n    onAccept?.();\n    setRun(true);\n    setFlow(\"applied\");\n  };\n  const handleReject = (e: React.MouseEvent<HTMLButtonElement>) => {\n    if (!interactive) return;\n    fromKeyboard(e);\n    onReject?.();\n    setManual(false);\n    setRun(false);\n    setFlow(\"idle\");\n  };\n\n  const typography: React.CSSProperties = {\n    fontFamily: FONT_FALLBACK,\n    lineHeight: 1.6,\n    ...font,\n    margin: 0,\n  };\n  const dockText: React.CSSProperties = {\n    fontFamily: FONT_FALLBACK,\n    ...font,\n    fontSize: dockPx,\n    lineHeight: 1.2,\n    margin: 0,\n  };\n  const pad = Math.max(0, padding);\n  const dockW = Math.ceil(DOCK_PAD + LEAD + DOCK_GAP + sizes[shownFlow] + DOCK_PAD);\n  const shimmerGradient = `linear-gradient(90deg, ${mutedColor} 0%, ${mutedColor} 32%, ${textColor} 50%, ${mutedColor} 68%, ${mutedColor} 100%)`;\n  const softRing = Math.max(2, Math.min(14, ringSize * 0.45));\n  const softBlur = ringSize * 0.7;\n  const auraRgb = accents[shownFlow].join(\",\");\n  const live4 = interactive;\n  const working = shownFlow === \"working\";\n  const statusLine =\n    shownFlow === \"idle\"\n      ? actionList.map((a) => a.label).join(\", \")\n      : shownFlow === \"working\"\n        ? `${workingText}: ${current.label}`\n        : shownFlow === \"review\"\n          ? `${reviewText}: ${insCount} ${insCount === 1 ? \"word\" : \"words\"} added, ${delCount} removed`\n          : appliedText;\n\n  const shimmerStyle: React.CSSProperties = {\n    color: \"transparent\",\n    backgroundImage: shimmerGradient,\n    backgroundSize: \"300% 100%\",\n    backgroundPosition: `${STATIC_SHIMMER}% 50%`,\n    WebkitBackgroundClip: \"text\",\n    backgroundClip: \"text\",\n    WebkitTextFillColor: \"transparent\",\n  };\n\n  const ringLayer = (idx: number, on: boolean, gradient: string, intensity: number) => (\n    <div\n      key={idx}\n      aria-hidden=\"true\"\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        zIndex: 3,\n        borderRadius: \"inherit\",\n        pointerEvents: \"none\",\n        opacity: on ? intensity : 0,\n        transition: \"opacity 0.6s ease\",\n      }}\n    >\n      <div\n        ref={(el) => {\n          ringEls.current[idx] = el;\n        }}\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          borderRadius: \"inherit\",\n          opacity: 0.55,\n        }}\n      >\n        <div style={{ position: \"absolute\", inset: 0, borderRadius: \"inherit\", filter: `blur(${softBlur}px)` }}>\n          <div style={{ ...ringStyle(softRing), background: gradient }} />\n        </div>\n        <div style={{ ...ringStyle(1.5), background: gradient }} />\n      </div>\n    </div>\n  );\n\n  let insSeen = 0;\n  let delSeen = 0;\n  const passageWords: React.ReactNode[] = [];\n  if (reviewing) {\n    ops.forEach((op, i) => {\n      if (op.kind === \"eq\") {\n        passageWords.push(<span key={i}>{op.text + \" \"}</span>);\n      } else if (op.kind === \"del\") {\n        const k = delSeen++;\n        const gone = shownFlow === \"applied\";\n        const w = delW[k];\n        passageWords.push(\n          <span\n            key={i}\n            aria-hidden=\"true\"\n            ref={(el) => {\n              delEls.current[k] = el;\n            }}\n            style={{\n              display: \"inline-block\",\n              verticalAlign: \"bottom\",\n              whiteSpace: \"pre\",\n              overflow: \"hidden\",\n              maxWidth: gone ? 0 : w ? w : \"none\",\n              opacity: gone ? 0 : 1,\n              color: rgba(delRgb, 0.95),\n              background: rgba(delRgb, 0.14),\n              textDecoration: \"line-through\",\n              textDecorationColor: rgba(delRgb, 0.7),\n              borderRadius: 4,\n              transition: \"max-width 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease\",\n            }}\n          >\n            {op.text + \" \"}\n          </span>,\n        );\n      } else {\n        const k = insSeen++;\n        if (k >= shownIns) return;\n        const fresh = shownFlow === \"review\" && k === shownIns - 1 && shownIns < insCount;\n        const done = shownFlow === \"applied\";\n        passageWords.push(\n          <span\n            key={i}\n            style={{\n              color: done ? textColor : rgba(addRgb, 1),\n              background: done ? rgba(addRgb, 0) : rgba(addRgb, 0.14),\n              borderRadius: 4,\n              opacity: fresh ? 0.45 : 1,\n              transition: \"opacity 0.25s ease, background-color 0.6s ease, color 0.6s ease\",\n            }}\n          >\n            {op.text + \" \"}\n          </span>,\n        );\n      }\n    });\n  }\n  const highlighted = shownFlow === \"idle\" || working;\n  const passageNode = (\n    <span\n      ref={(el) => {\n        textEls.current[0] = working ? el : null;\n      }}\n      style={{\n        color: textColor,\n        backgroundColor: highlighted ? rgba(selRgb, 0.3) : rgba(selRgb, 0),\n        borderRadius: 5,\n        boxDecorationBreak: \"clone\",\n        WebkitBoxDecorationBreak: \"clone\",\n        padding: \"1px 0\",\n        transition: \"background-color 0.4s ease\",\n        ...(working ? shimmerStyle : {}),\n      }}\n    >\n      {reviewing ? passageWords : selected}\n    </span>\n  );\n\n  const group = (f: Flow, content: React.ReactNode) => {\n    const on = f === shownFlow;\n    return (\n      <div\n        key={f}\n        ref={(el) => {\n          groupEls.current[f] = el;\n        }}\n        data-flow={f}\n        aria-hidden={!on}\n        style={{\n          position: \"absolute\",\n          left: DOCK_PAD + LEAD + DOCK_GAP,\n          top: DOCK_PAD,\n          height: LEAD,\n          width: \"max-content\",\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 6,\n          visibility: on ? \"visible\" : \"hidden\",\n          opacity: on ? 1 : 0,\n          transform: `translateY(${on ? 0 : 6}px)`,\n          transition: on\n            ? \"opacity 0.35s ease 0.18s, transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) 0.18s, visibility 0s linear 0s\"\n            : \"opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.16s\",\n          pointerEvents: on && live4 ? \"auto\" : \"none\",\n        }}\n      >\n        {content}\n      </div>\n    );\n  };\n\n  const orbNode = (\n    <div\n      aria-hidden=\"true\"\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        width: ORB_SIDE,\n        height: ORB_SIDE,\n        transformOrigin: \"0 0\",\n        transform: `translate(${(DOCK_PAD + LEAD / 2 - (ORB_SIDE * ((LEAD * 0.9) / (ORB_RATIO * ORB_SIDE))) / 2).toFixed(2)}px, ${(DOCK_H / 2 - (ORB_SIDE * ((LEAD * 0.9) / (ORB_RATIO * ORB_SIDE))) / 2).toFixed(2)}px) scale(${((LEAD * 0.9) / (ORB_RATIO * ORB_SIDE)).toFixed(4)})`,\n        opacity: shownFlow === \"applied\" ? 0 : 1,\n        transition: \"opacity 0.3s ease\",\n        pointerEvents: \"none\",\n      }}\n    >\n      {orbAura > 0 && (\n        <div\n          ref={haloRef}\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            borderRadius: \"50%\",\n            background: `radial-gradient(circle, rgba(${auraRgb},0.4) 0%, rgba(${auraRgb},0) 62%)`,\n            opacity: orbAura * AURA_GAIN * 0.2,\n          }}\n        />\n      )}\n      <canvas\n        ref={canvasRef}\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          width: ORB_SIDE,\n          height: ORB_SIDE,\n          display: \"block\",\n        }}\n      />\n    </div>\n  );\n\n  const checkBadge = (\n    <div\n      aria-hidden=\"true\"\n      style={{\n        position: \"absolute\",\n        left: DOCK_PAD + 3,\n        top: DOCK_PAD + 3,\n        width: LEAD - 6,\n        height: LEAD - 6,\n        borderRadius: \"50%\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        color: rgba(doneRgb, 1),\n        background: \"rgba(255,255,255,0.07)\",\n        border: `1px solid ${HAIRLINE}`,\n        boxShadow: `0 0 14px ${rgba(doneRgb, 0.28)}`,\n        opacity: shownFlow === \"applied\" ? 1 : 0,\n        transform: shownFlow === \"applied\" ? \"scale(1)\" : \"scale(0.5)\",\n        transition: \"opacity 0.3s ease 0.1s, transform 0.5s cubic-bezier(0.3, 1.4, 0.5, 1) 0.1s\",\n      }}\n    >\n      <CheckIcon />\n    </div>\n  );\n\n  return (\n    <article\n      ref={rootRef}\n      role=\"region\"\n      aria-label=\"AI text editor\"\n      className={cn(\"relative box-border flex h-full w-full flex-col overflow-hidden\", className)}\n      style={{\n        ...style,\n        gap,\n        padding: pad,\n        borderRadius: radius,\n        background,\n        backgroundImage: SHEEN,\n        boxShadow: `inset 0 0 0 1px ${borderColor}, inset 0 1px 0 rgba(255,255,255,0.1)`,\n        backdropFilter: \"blur(14px)\",\n        WebkitBackdropFilter: \"blur(14px)\",\n      }}\n    >\n      <p style={SR_ONLY}>{description}</p>\n      <span role=\"status\" aria-live=\"polite\" style={SR_ONLY}>\n        {statusLine}\n      </span>\n\n      {ringLayer(0, working, workGradient, ringIntensity)}\n      {ringLayer(1, shownFlow === \"review\", reviewGradient, ringIntensity * 0.45)}\n      {ringLayer(2, shownFlow === \"applied\", doneGradient, ringIntensity * 0.6)}\n\n      <div\n        style={{\n          position: \"relative\",\n          zIndex: 1,\n          flex: 1,\n          minHeight: 0,\n          overflow: \"hidden\",\n        }}\n      >\n        <p style={{ ...typography, color: mutedColor, wordBreak: \"break-word\" }}>\n          {before}\n          {passageNode}\n          {after}\n        </p>\n      </div>\n\n      <div\n        style={{\n          position: \"relative\",\n          zIndex: 1,\n          flex: \"none\",\n          display: \"flex\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div\n          style={{\n            position: \"relative\",\n            flex: \"none\",\n            width: dockW,\n            maxWidth: \"100%\",\n            height: DOCK_H,\n            overflow: \"hidden\",\n            borderRadius: 999,\n            background: \"rgba(255,255,255,0.05)\",\n            boxShadow: `inset 0 0 0 1px ${HAIRLINE}, inset 0 1px 0 rgba(255,255,255,0.08)`,\n            transition: `width ${MORPH}`,\n          }}\n        >\n          {orbNode}\n          {checkBadge}\n\n          {group(\n            \"idle\",\n            actionList.map((a, i) => (\n              <DockAction\n                key={`action-${i}`}\n                label={a.label}\n                onClick={handleAction(i)}\n                live={live4 && shownFlow === \"idle\"}\n                variant=\"ghost\"\n                textStyle={dockText}\n                textColor={textColor}\n                extra={{ primary: i === 0 }}\n              />\n            )),\n          )}\n          {group(\n            \"working\",\n            <>\n              <span\n                style={{\n                  ...dockText,\n                  ...shimmerStyle,\n                  whiteSpace: \"nowrap\",\n                  marginRight: 4,\n                }}\n                ref={(el) => {\n                  textEls.current[1] = el;\n                }}\n              >\n                {workingText}\n              </span>\n              <DockAction\n                label=\"Cancel\"\n                onClick={handleCancel}\n                live={live4 && shownFlow === \"working\"}\n                variant=\"round\"\n                textStyle={dockText}\n                textColor={textColor}\n                extra={{ icon: <StopIcon />, primary: true, ariaLabel: \"Cancel rewrite\" }}\n              />\n            </>,\n          )}\n          {group(\n            \"review\",\n            <>\n              <span\n                style={{\n                  ...dockText,\n                  whiteSpace: \"nowrap\",\n                  marginRight: 4,\n                  display: \"flex\",\n                  gap: 8,\n                  color: mutedColor,\n                }}\n                title={reviewText}\n              >\n                <span style={{ color: rgba(addRgb, 1) }}>{`+${insCount}`}</span>\n                <span style={{ color: rgba(delRgb, 1) }}>{`−${delCount}`}</span>\n              </span>\n              <DockAction\n                label={rejectLabel}\n                onClick={handleReject}\n                live={live4 && shownFlow === \"review\"}\n                variant=\"soft\"\n                textStyle={dockText}\n                textColor={textColor}\n                extra={{ icon: <CloseIcon /> }}\n              />\n              <DockAction\n                label={acceptLabel}\n                onClick={handleAccept}\n                live={live4 && shownFlow === \"review\"}\n                variant=\"primary\"\n                textStyle={dockText}\n                textColor={textColor}\n                extra={{ icon: <CheckIcon />, primary: true }}\n              />\n            </>,\n          )}\n          {group(\n            \"applied\",\n            <span\n              style={{\n                ...dockText,\n                whiteSpace: \"nowrap\",\n                color: textColor,\n                paddingRight: 4,\n              }}\n            >\n              {appliedText}\n            </span>,\n          )}\n        </div>\n      </div>\n    </article>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}