// Sound card grid, master controls, circular timer, presets,
// custom sounds bar/card, recording modal, update banner — TypeScript

declare const React: any;
declare const SOUNDS: any[];
declare const GROUP_HUE: Record<string, string>;

type LangT = 'ko' | 'en';
type ThemeT = 'light' | 'dark' | 'sepia';

interface SoundT {
  id: string; group: string; emoji: string; ko: string; en: string;
  file?: string; meta_ko?: string; meta_en?: string;
  isCustom?: boolean;
  loopMode?: 'continuous' | 'interval';
  intervalSec?: number;
}
interface PresetT {
  id: string; ko: string; en: string; icon?: string;
  mix: Record<string, number>; custom?: boolean;
}
interface StringsT {
  appTitle: string; appSubtitle: string; tagline: string; badges: string[];
  stopAll: string; nowPlaying: string; nothingPlaying: string; pickToStart: string;
  presets: string; presetsHint: string; saveMix: string; sounds: string; soundsHint: string;
  activeSounds: (n: number) => string;
  cancel: string; save: string; namePreset: string;
  customSection: string; addFile: string; recordVoice: string; customNoneHint: string;
  recHint: string; recStart: string; recStop: string; recRetry: string;
  loopMode: string; continuous: string; interval: string; intervalSec: string;
  customName: string; delete: string;
  updateAvailable: string; applyUpdate: string;
  [k: string]: any;
}

// ── Circular Timer Dial ─────────────────────────────────────────────────────
interface CircularTimerProps {
  minutes: number | null;
  remaining: number | null;
  running: boolean;
  onChange: (m: number | null) => void;
  onCancel: () => void;
  lang: LangT;
  theme: ThemeT;
}
function CircularTimer({ minutes, remaining, running, onChange, onCancel, lang }: CircularTimerProps) {
  const SIZE = 220;
  const R = 86;
  const STROKE = 14;
  const cx = SIZE / 2, cy = SIZE / 2;
  const max = 240;
  const dialRef = React.useRef<SVGSVGElement | null>(null);
  const [dragging, setDragging] = React.useState<boolean>(false);

  const angle = ((minutes || 0) / max) * 360;
  const progressAngle = running && remaining != null && minutes
    ? (remaining / (minutes * 60)) * angle
    : angle;

  const polar = (a: number, r: number): [number, number] => {
    const rad = (a - 90) * Math.PI / 180;
    return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
  };
  const arc = (a: number): string => {
    const [x, y] = polar(a, R);
    const large = a > 180 ? 1 : 0;
    return `M ${cx} ${cy - R} A ${R} ${R} 0 ${large} 1 ${x} ${y}`;
  };

  const angleFromEvent = (e: PointerEvent | React.PointerEvent): number => {
    const rect = dialRef.current!.getBoundingClientRect();
    const x = (e as PointerEvent).clientX - rect.left - SIZE / 2;
    const y = (e as PointerEvent).clientY - rect.top - SIZE / 2;
    let a = Math.atan2(y, x) * 180 / Math.PI + 90;
    if (a < 0) a += 360;
    return a;
  };

  const onPointerDown = (e: React.PointerEvent<SVGSVGElement>): void => {
    if (running) return;
    setDragging(true);
    const update = (ev: PointerEvent | React.PointerEvent): void => {
      const a = angleFromEvent(ev);
      let mins = Math.round((a / 360) * max / 5) * 5;
      if (mins < 5) mins = 0;
      if (mins > max) mins = max;
      onChange(mins === 0 ? null : mins);
    };
    update(e);
    const move = (ev: PointerEvent) => update(ev);
    const up = () => {
      setDragging(false);
      window.removeEventListener('pointermove', move);
      window.removeEventListener('pointerup', up);
    };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  const fmt = (m: number | null): string => {
    if (m == null || m === 0) return lang === 'ko' ? '계속' : '∞';
    if (m >= 60) {
      const h = Math.floor(m / 60), r = m % 60;
      return r ? `${h}:${String(r).padStart(2, '0')}` : `${h}h`;
    }
    return `${m}m`;
  };
  const fmtRemaining = (s: number | null): string | null => {
    if (s == null) return null;
    const m = Math.floor(s / 60), sec = s % 60;
    return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
  };

  const ticks: any[] = [];
  for (let m = 0; m <= max; m += 30) {
    const a = (m / max) * 360;
    const [x1, y1] = polar(a, R - STROKE / 2 - 4);
    const [x2, y2] = polar(a, R - STROKE / 2 - 12);
    ticks.push(<line key={m} x1={x1} y1={y1} x2={x2} y2={y2} stroke="currentColor" strokeWidth="1" opacity="0.18" />);
  }
  const labels: Array<{m: number; ko: string; en: string}> = [
    { m: 0,   ko: '계속', en: '∞' },
    { m: 30,  ko: '30',   en: '30' },
    { m: 60,  ko: '1h',   en: '1h' },
    { m: 120, ko: '2h',   en: '2h' },
    { m: 180, ko: '3h',   en: '3h' },
  ];

  const [hx, hy] = polar(angle, R);

  return (
    <div className="timer-dial-wrap">
      <svg ref={dialRef} className={`timer-dial ${dragging ? 'dragging' : ''} ${running ? 'running' : ''}`}
           width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`}
           onPointerDown={onPointerDown} style={{ touchAction: 'none' }}>
        <defs>
          <linearGradient id="timerGrad" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%" stopColor="var(--accent-peach)" />
            <stop offset="100%" stopColor="var(--accent-lavender)" />
          </linearGradient>
        </defs>
        <circle cx={cx} cy={cy} r={R} fill="none" stroke="var(--track)" strokeWidth={STROKE} />
        {ticks}
        {labels.map(({ m, ko, en }) => {
          const a = (m / max) * 360;
          const [lx, ly] = polar(a, R + 22);
          return (
            <text key={m} x={lx} y={ly + 4} textAnchor="middle" fontSize="10"
                  fill="currentColor" opacity="0.5" fontWeight="500">
              {lang === 'ko' ? ko : en}
            </text>
          );
        })}
        {angle > 0 && (
          <path d={arc(angle)} fill="none" stroke="url(#timerGrad)"
                strokeWidth={STROKE} strokeLinecap="round" opacity={running ? 0.25 : 1} />
        )}
        {running && progressAngle > 0 && (
          <path d={arc(progressAngle)} fill="none" stroke="url(#timerGrad)"
                strokeWidth={STROKE} strokeLinecap="round" />
        )}
        {angle > 0 && !running && (
          <circle cx={hx} cy={hy} r="10" fill="white" stroke="var(--accent-peach)" strokeWidth="2" />
        )}
        <g style={{ pointerEvents: 'none' }}>
          {running ? (
            <>
              <text x={cx} y={cy - 4} textAnchor="middle" fontSize="11"
                    fill="currentColor" opacity="0.55" letterSpacing="0.05em">
                {lang === 'ko' ? '남은 시간' : 'TIME LEFT'}
              </text>
              <text x={cx} y={cy + 22} textAnchor="middle" fontSize="28"
                    fill="currentColor" fontWeight="600" fontVariantNumeric="tabular-nums"
                    letterSpacing="0.02em">
                {fmtRemaining(remaining)}
              </text>
            </>
          ) : (
            <>
              <text x={cx} y={cy - 6} textAnchor="middle" fontSize="11"
                    fill="currentColor" opacity="0.55" letterSpacing="0.05em">
                {lang === 'ko' ? '타이머' : 'TIMER'}
              </text>
              <text x={cx} y={cy + 22} textAnchor="middle" fontSize="32"
                    fill="currentColor" fontWeight="600" letterSpacing="0.02em">
                {fmt(minutes)}
              </text>
            </>
          )}
        </g>
      </svg>
      {running && (
        <button className="timer-cancel" onClick={onCancel}>
          {lang === 'ko' ? '취소' : 'Cancel'}
        </button>
      )}
    </div>
  );
}

// ── Sound Card ──────────────────────────────────────────────────────────────
interface SoundCardProps {
  sound: SoundT;
  active: boolean;
  volume: number;
  onToggle: () => void;
  onVolume: (v: number) => void;
  lang: LangT;
  shape: 'rounded' | 'circle' | 'tile';
  anim: 'breathe' | 'wave' | 'glow' | 'static';
  density: 'compact' | 'comfy';
}
function SoundCard({ sound, active, volume, onToggle, onVolume, lang, shape, anim, density }: SoundCardProps) {
  const [showVol, setShowVol] = React.useState<boolean>(false);
  const longPressRef = React.useRef<number | null>(null);

  const onPointerDown = (): void => {
    longPressRef.current = window.setTimeout(() => {
      if (active) setShowVol(true);
    }, 400);
  };
  const cancelLong = (): void => {
    if (longPressRef.current != null) {
      clearTimeout(longPressRef.current);
      longPressRef.current = null;
    }
  };
  const onClick = (): void => {
    cancelLong();
    if (showVol) { setShowVol(false); return; }
    onToggle();
  };

  const label = lang === 'ko' ? sound.ko : sound.en;
  const groupHue = GROUP_HUE[sound.group] || 'peach';

  return (
    <button
      className={`sound-card shape-${shape} anim-${anim} density-${density} ${active ? 'active' : ''} hue-${groupHue}`}
      onPointerDown={onPointerDown}
      onPointerUp={cancelLong}
      onPointerLeave={cancelLong}
      onClick={onClick}
      aria-pressed={active}
    >
      <div className="sound-card-glow" />
      <div className="sound-card-body">
        <div className="sound-emoji">{sound.emoji}</div>
        <div className="sound-label">{label}</div>
        {active && anim === 'wave' && (
          <div className="sound-waveform">
            {[0,1,2,3,4].map((i: number) => <span key={i} style={{ animationDelay: `${i * 0.12}s` }} />)}
          </div>
        )}
        {active && (
          <div className="sound-volume-bar">
            <div className="sound-volume-fill" style={{ width: `${volume * 100}%` }} />
          </div>
        )}
      </div>
      {showVol && (
        <div className="sound-vol-popover" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
          <div className="sound-vol-label">{lang === 'ko' ? '볼륨' : 'Volume'}</div>
          <input type="range" min="0" max="1" step="0.01" value={volume}
                 onChange={(e: React.ChangeEvent<HTMLInputElement>) => onVolume(Number(e.target.value))} />
          <button className="sound-vol-close" onClick={() => setShowVol(false)}>✓</button>
        </div>
      )}
    </button>
  );
}

// ── Custom Sound Card (with settings popover) ──────────────────────────────
interface CustomSoundCardProps {
  sound: SoundT;
  active: boolean;
  volume: number;
  onToggle: () => void;
  onVolume: (v: number) => void;
  onSaveSettings: (name: string, loopMode: 'continuous' | 'interval', intervalSec: number) => void;
  onDelete: () => void;
  lang: LangT;
  shape: 'rounded' | 'circle' | 'tile';
  anim: 'breathe' | 'wave' | 'glow' | 'static';
  density: 'compact' | 'comfy';
  t: StringsT;
}
function CustomSoundCard({ sound, active, volume, onToggle, onVolume, onSaveSettings, onDelete, lang, shape, anim, density, t }: CustomSoundCardProps) {
  const [showSettings, setShowSettings] = React.useState<boolean>(false);
  const [name, setName] = React.useState<string>(lang === 'ko' ? sound.ko : sound.en);
  const [loopMode, setLoopMode] = React.useState<'continuous' | 'interval'>(sound.loopMode || 'continuous');
  const [intervalSec, setIntervalSec] = React.useState<number>(sound.intervalSec || 30);

  React.useEffect(() => {
    setName(lang === 'ko' ? sound.ko : sound.en);
    setLoopMode(sound.loopMode || 'continuous');
    setIntervalSec(sound.intervalSec || 30);
  }, [sound.id, sound.ko, sound.en, sound.loopMode, sound.intervalSec, lang]);

  const onClickCard = (): void => {
    if (showSettings) return;
    onToggle();
  };
  const onSave = (): void => {
    const trimmed = (name || '').trim();
    const safeInt = Math.max(1, Math.min(3600, intervalSec || 30));
    onSaveSettings(trimmed || sound.ko, loopMode, safeInt);
    setShowSettings(false);
  };
  const onConfirmDelete = (): void => {
    const label = lang === 'ko' ? sound.ko : sound.en;
    const msg = lang === 'ko' ? `"${label}" 음원을 삭제할까요?` : `Delete "${label}"?`;
    if (window.confirm(msg)) onDelete();
  };

  const groupHue = 'rose';
  const labelText = lang === 'ko' ? sound.ko : sound.en;

  return (
    <div className={`sound-card custom-sound-card shape-${shape} anim-${anim} density-${density} ${active ? 'active' : ''} hue-${groupHue}`}
         aria-pressed={active}>
      <button className="sound-card-clickarea" onClick={onClickCard} type="button">
        <div className="sound-card-glow" />
        <div className="sound-card-body">
          <div className="sound-emoji">{sound.emoji}</div>
          <div className="sound-label">{labelText}</div>
          {active && anim === 'wave' && (
            <div className="sound-waveform">
              {[0,1,2,3,4].map((i: number) => <span key={i} style={{ animationDelay: `${i * 0.12}s` }} />)}
            </div>
          )}
          {active && (
            <div className="sound-volume-bar">
              <div className="sound-volume-fill" style={{ width: `${volume * 100}%` }} />
            </div>
          )}
        </div>
      </button>
      <button className="custom-gear-btn" type="button" title="Settings"
              onClick={(e: React.MouseEvent) => { e.stopPropagation(); setShowSettings((s: boolean) => !s); }}>⚙</button>
      {showSettings && (
        <div className="sound-vol-popover custom-settings-popover"
             onClick={(e: React.MouseEvent) => e.stopPropagation()}>
          <label className="custom-field">
            <span>{t.customName}</span>
            <input type="text" value={name}
                   onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)} />
          </label>
          <label className="custom-field">
            <span>{t.loopMode}</span>
            <select value={loopMode}
                    onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setLoopMode(e.target.value as any)}>
              <option value="continuous">{t.continuous}</option>
              <option value="interval">{t.interval}</option>
            </select>
          </label>
          {loopMode === 'interval' && (
            <label className="custom-field">
              <span>{t.intervalSec}</span>
              <input type="number" min="1" max="3600" step="1" value={intervalSec}
                     onChange={(e: React.ChangeEvent<HTMLInputElement>) => setIntervalSec(Number(e.target.value))} />
            </label>
          )}
          <label className="custom-field">
            <span>{t.volume}</span>
            <input type="range" min="0" max="1" step="0.01" value={volume}
                   onChange={(e: React.ChangeEvent<HTMLInputElement>) => onVolume(Number(e.target.value))} />
          </label>
          <div className="custom-settings-actions">
            <button className="btn-secondary" onClick={onConfirmDelete}>{t.delete}</button>
            <button className="btn-primary" onClick={onSave}>{t.save}</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ── Custom Sounds Bar (📁 file / 🎤 record) ────────────────────────────────
interface CustomSoundsBarProps {
  onAddFile: (file: File) => void;
  onOpenRecord: () => void;
  lang: LangT;
  t: StringsT;
}
function CustomSoundsBar({ onAddFile, onOpenRecord, lang, t }: CustomSoundsBarProps) {
  const fileRef = React.useRef<HTMLInputElement | null>(null);
  return (
    <div className="custom-sounds-bar">
      <button className="custom-add-btn" onClick={() => fileRef.current?.click()}>{t.addFile}</button>
      <button className="custom-add-btn" onClick={onOpenRecord}>{t.recordVoice}</button>
      <input ref={fileRef} type="file" accept="audio/*" hidden
             onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
               const f = e.target.files && e.target.files[0];
               e.target.value = '';
               if (f) onAddFile(f);
             }} />
    </div>
  );
}

// ── Master Controls (top bar) ───────────────────────────────────────────────
interface MasterBarProps {
  lang: LangT;
  masterVol: number;
  onMaster: (v: number) => void;
  activeCount: number;
  onStopAll: () => void;
  onLangToggle: () => void;
  onThemeToggle: () => void;
  onRefresh: () => void;
  onOpenTweaks: () => void;
  theme: ThemeT;
  t: StringsT;
}
function MasterBar({ lang, masterVol, onMaster, activeCount, onStopAll, onLangToggle, onThemeToggle, onRefresh, onOpenTweaks, theme, t }: MasterBarProps) {
  return (
    <header className="master-bar">
      <div className="brand">
        <div className="brand-mark">
          <svg width="36" height="36" viewBox="0 0 36 36" role="img" aria-label={lang === 'ko' ? '아기 백색소음 믹서 로고' : 'Baby Sleep Mixer logo'}>
            <title>{lang === 'ko' ? '아기 백색소음 믹서' : 'Baby Sleep Mixer'}</title>
            <defs>
              <linearGradient id="moonG" x1="0" y1="0" x2="1" y2="1">
                <stop offset="0%" stopColor="var(--accent-peach)" />
                <stop offset="100%" stopColor="var(--accent-lavender)" />
              </linearGradient>
            </defs>
            <circle cx="18" cy="18" r="16" fill="url(#moonG)" opacity="0.15" />
            <path d="M22 9 A 11 11 0 1 0 27 22 A 8.5 8.5 0 0 1 22 9 Z" fill="url(#moonG)" />
            <circle cx="11" cy="13" r="0.8" fill="currentColor" opacity="0.4" />
            <circle cx="9"  cy="22" r="0.6" fill="currentColor" opacity="0.3" />
            <circle cx="14" cy="27" r="0.7" fill="currentColor" opacity="0.35" />
          </svg>
        </div>
        <div className="brand-text">
          <div className="brand-title">{t.appTitle}</div>
          <div className="brand-sub">{t.appSubtitle}</div>
        </div>
      </div>

      <div className="master-controls">
        <div className="master-vol">
          <span className="master-vol-icon">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
              <path d="M3 6h2l3-2.5v9L5 10H3V6z" fill="currentColor"/>
              <path d="M10 6c.8.5 1.3 1.3 1.3 2s-.5 1.5-1.3 2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" fill="none"/>
              <path d="M11.5 4c1.5 1 2.5 2.5 2.5 4s-1 3-2.5 4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" fill="none" opacity="0.6"/>
            </svg>
          </span>
          <input className="master-slider" type="range" min="0" max="1" step="0.01"
                 value={masterVol} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onMaster(Number(e.target.value))} />
          <span className="master-vol-num">{Math.round(masterVol * 100)}</span>
        </div>

        <button className="icon-btn" onClick={onLangToggle} title="Language">
          <span style={{ fontWeight: 600, fontSize: 12 }}>{lang === 'ko' ? '한' : 'EN'}</span>
        </button>
        <button className="icon-btn" onClick={onThemeToggle} title="Theme">
          {theme === 'dark' ? '☾' : '☼'}
        </button>
        <button className="icon-btn" onClick={onRefresh} title={lang === 'ko' ? '최신 버전으로' : 'Refresh'}>↻</button>
        <button className="icon-btn" onClick={onOpenTweaks} title={lang === 'ko' ? '설정' : 'Settings'}>⚙</button>

        <button className={`stop-all-btn ${activeCount > 0 ? 'is-active' : ''}`}
                onClick={onStopAll} disabled={activeCount === 0}>
          <span className="stop-icon">■</span>
          <span>{t.stopAll}</span>
          {activeCount > 0 && <span className="stop-count">{activeCount}</span>}
        </button>
      </div>
    </header>
  );
}

// ── Now Playing strip ───────────────────────────────────────────────────────
interface NowPlayingProps {
  activeSounds: string[];
  sounds: SoundT[];
  volumes: Record<string, number>;
  onVolume: (id: string, v: number) => void;
  onRemove: (id: string) => void;
  lang: LangT;
  t: StringsT;
}
function NowPlaying({ activeSounds, sounds, volumes, onVolume, onRemove, lang, t }: NowPlayingProps) {
  if (activeSounds.length === 0) {
    return (
      <div className="now-playing empty">
        <div className="np-empty-icon">
          <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
            <circle cx="18" cy="18" r="16" stroke="currentColor" strokeWidth="1" strokeDasharray="2 3" opacity="0.3"/>
            <circle cx="18" cy="18" r="3" fill="currentColor" opacity="0.3"/>
          </svg>
        </div>
        <div>
          <div className="np-empty-title">{t.nothingPlaying}</div>
          <div className="np-empty-sub">{t.pickToStart}</div>
        </div>
      </div>
    );
  }
  return (
    <div className="now-playing">
      <div className="np-header">
        <span className="np-title">
          <span className="np-pulse" />
          {t.nowPlaying}
        </span>
        <span className="np-count">{t.activeSounds(activeSounds.length)}</span>
      </div>
      <div className="np-list">
        {activeSounds.map((id: string) => {
          const s = sounds.find((x: SoundT) => x.id === id);
          if (!s) return null;
          return (
            <div key={id} className="np-row">
              <div className="np-emoji">{s.emoji}</div>
              <div className="np-name">{lang === 'ko' ? s.ko : s.en}</div>
              <input type="range" min="0" max="1" step="0.01"
                     value={volumes[id] ?? 0.5}
                     onChange={(e: React.ChangeEvent<HTMLInputElement>) => onVolume(id, Number(e.target.value))}
                     className="np-slider" />
              <span className="np-vol">{Math.round((volumes[id] ?? 0.5) * 100)}</span>
              <button className="np-remove" onClick={() => onRemove(id)} aria-label="Remove">×</button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── Presets ────────────────────────────────────────────────────────────────
interface PresetsRowProps {
  presets: PresetT[];
  customPresets: PresetT[];
  onApply: (p: PresetT) => void;
  onSave: () => void;
  onDelete: (id: string) => void;
  currentMix: Record<string, number>;
  sounds: SoundT[];
  lang: LangT;
  t: StringsT;
}
function PresetChip({ p, sounds, lang, onApply, onDelete }: {
  p: PresetT; sounds: SoundT[]; lang: LangT;
  onApply: (p: PresetT) => void; onDelete: (id: string) => void;
}) {
  return (
    <button className={`preset-chip ${p.custom ? 'custom' : ''}`}
            onClick={() => onApply(p)}>
      <div className="preset-chip-icon">{p.icon || '⭐'}</div>
      <div className="preset-chip-body">
        <div className="preset-chip-name">{lang === 'ko' ? p.ko : p.en}</div>
        <div className="preset-chip-mix">
          {Object.keys(p.mix).slice(0, 4).map((id: string) => {
            const s = sounds.find((x: SoundT) => x.id === id);
            return s ? <span key={id}>{s.emoji}</span> : null;
          })}
          {Object.keys(p.mix).length > 4 && <span className="preset-chip-more">+{Object.keys(p.mix).length - 4}</span>}
        </div>
      </div>
      {p.custom && (
        <span className="preset-chip-del"
              onClick={(e: React.MouseEvent) => { e.stopPropagation(); onDelete(p.id); }}>×</span>
      )}
    </button>
  );
}

function PresetsRow({ presets, customPresets, onApply, onSave, onDelete, currentMix, sounds, lang, t }: PresetsRowProps) {
  return (
    <div className="presets">
      <div className="presets-header">
        <div>
          <div className="section-title">{t.presets}</div>
          <div className="section-hint">{t.presetsHint}</div>
        </div>
        <button className="save-mix-btn" onClick={onSave}
                disabled={Object.keys(currentMix).length === 0}>
          <span>＋</span>
          {t.saveMix}
        </button>
      </div>
      <div className="presets-grid">
        {presets.map((p: PresetT) => (
          <PresetChip key={p.id} p={p} sounds={sounds} lang={lang} onApply={onApply} onDelete={onDelete} />
        ))}
      </div>
      {customPresets.length > 0 && (
        <>
          <div className="presets-subheader">
            <span>★ {t.myPresets}</span>
            <span className="presets-subheader-count">{customPresets.length}</span>
          </div>
          <div className="presets-grid">
            {customPresets.map((p: PresetT) => (
              <PresetChip key={p.id} p={p} sounds={sounds} lang={lang} onApply={onApply} onDelete={onDelete} />
            ))}
          </div>
        </>
      )}
    </div>
  );
}

// ── Recording Modal ─────────────────────────────────────────────────────────
interface RecordModalProps {
  open: boolean;
  onClose: () => void;
  onSave: (blob: Blob, defaultName: string) => Promise<void> | void;
  lang: LangT;
  t: StringsT;
}
function RecordModal({ open, onClose, onSave, lang, t }: RecordModalProps) {
  const [phase, setPhase] = React.useState<'idle' | 'recording' | 'preview'>('idle');
  const [elapsedMs, setElapsedMs] = React.useState<number>(0);
  const [blob, setBlob] = React.useState<Blob | null>(null);
  const [previewUrl, setPreviewUrl] = React.useState<string>('');
  const [hint, setHint] = React.useState<string>(t.recHint);
  const recorderRef = React.useRef<MediaRecorder | null>(null);
  const chunksRef = React.useRef<Blob[]>([]);
  const streamRef = React.useRef<MediaStream | null>(null);
  const tickRef = React.useRef<number | null>(null);
  const startedAtRef = React.useRef<number>(0);

  React.useEffect(() => {
    if (!open) {
      // cleanup on close
      reset();
    } else {
      setHint(t.recHint);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open]);

  React.useEffect(() => () => reset(), []);

  function reset() {
    if (tickRef.current != null) { clearInterval(tickRef.current); tickRef.current = null; }
    if (recorderRef.current && recorderRef.current.state !== 'inactive') {
      try { recorderRef.current.stop(); } catch (e) {}
    }
    if (streamRef.current) { streamRef.current.getTracks().forEach((t: MediaStreamTrack) => t.stop()); streamRef.current = null; }
    if (previewUrl) { try { URL.revokeObjectURL(previewUrl); } catch (e) {} }
    chunksRef.current = [];
    setPhase('idle'); setElapsedMs(0); setBlob(null); setPreviewUrl('');
  }

  function pickMime(): string {
    const candidates = [
      'audio/mp4', 'audio/mp4;codecs=mp4a.40.2',
      'audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus',
    ];
    if (!('MediaRecorder' in window)) return '';
    for (const m of candidates) { if ((MediaRecorder as any).isTypeSupported(m)) return m; }
    return '';
  }

  async function start() {
    if (!('MediaRecorder' in window) || !navigator.mediaDevices) {
      setHint(lang === 'ko' ? '이 브라우저는 녹음을 지원하지 않습니다' : 'Recording not supported on this browser');
      return;
    }
    try {
      streamRef.current = await navigator.mediaDevices.getUserMedia({ audio: true });
    } catch (err: any) {
      setHint((lang === 'ko' ? '마이크 권한 거부됨: ' : 'Mic denied: ') + (err && err.message ? err.message : ''));
      return;
    }
    const mime = pickMime();
    try {
      recorderRef.current = mime
        ? new MediaRecorder(streamRef.current!, { mimeType: mime })
        : new MediaRecorder(streamRef.current!);
    } catch (err: any) {
      setHint((lang === 'ko' ? '녹음기 생성 실패: ' : 'Recorder init failed: ') + (err && err.message ? err.message : ''));
      streamRef.current.getTracks().forEach((t: MediaStreamTrack) => t.stop());
      streamRef.current = null;
      return;
    }
    chunksRef.current = [];
    recorderRef.current.ondataavailable = (e: BlobEvent) => { if (e.data && e.data.size) chunksRef.current.push(e.data); };
    recorderRef.current.onstop = () => {
      const type = recorderRef.current?.mimeType || mime || 'audio/webm';
      const out = new Blob(chunksRef.current, { type });
      const url = URL.createObjectURL(out);
      setBlob(out);
      setPreviewUrl(url);
      setPhase('preview');
      setHint(lang === 'ko' ? '미리듣기 후 저장하세요.' : 'Preview, then save.');
      if (streamRef.current) { streamRef.current.getTracks().forEach((t: MediaStreamTrack) => t.stop()); streamRef.current = null; }
    };
    recorderRef.current.start();
    startedAtRef.current = Date.now();
    setPhase('recording');
    setHint(lang === 'ko' ? '녹음 중입니다…' : 'Recording…');
    setElapsedMs(0);
    tickRef.current = window.setInterval(() => setElapsedMs(Date.now() - startedAtRef.current), 250);
  }
  function stop() {
    if (tickRef.current != null) { clearInterval(tickRef.current); tickRef.current = null; }
    if (recorderRef.current && recorderRef.current.state !== 'inactive') {
      try { recorderRef.current.stop(); } catch (e) {}
    }
  }
  function retry() { reset(); }
  async function save() {
    if (!blob) return;
    const defaultName = (lang === 'ko' ? '녹음 ' : 'Recording ') +
      new Date().toLocaleString(lang === 'ko' ? 'ko-KR' : 'en-US',
        { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
    const promptText = lang === 'ko' ? '녹음 이름:' : 'Recording name:';
    const name = (window.prompt(promptText, defaultName) || '').trim();
    if (!name) return;
    await onSave(blob, name);
    onClose();
  }

  if (!open) return null;
  const fmtTime = (ms: number): string => {
    const s = Math.floor(ms / 1000);
    return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal rec-modal" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
        <h3>{lang === 'ko' ? '🎤 음성 녹음' : '🎤 Record voice'}</h3>
        <p className="rec-hint">{hint}</p>
        <div className="rec-display">
          {phase === 'recording' && <span className="rec-dot" />}
          {fmtTime(elapsedMs)}
        </div>
        {phase === 'preview' && (
          <audio src={previewUrl} controls style={{ width: '100%' }} />
        )}
        <div className="modal-actions">
          <button className="btn-secondary" onClick={onClose}>{t.cancel}</button>
          {phase === 'recording' && <button className="btn-secondary" onClick={stop}>{t.recStop}</button>}
          {phase === 'idle' && <button className="btn-primary" onClick={start}>{t.recStart}</button>}
          {phase === 'preview' && <button className="btn-secondary" onClick={retry}>{t.recRetry}</button>}
          {phase === 'preview' && <button className="btn-primary" onClick={save}>{t.save}</button>}
        </div>
      </div>
    </div>
  );
}

// ── Update Banner ──────────────────────────────────────────────────────────
interface UpdateBannerProps {
  show: boolean;
  onApply: () => void;
  t: StringsT;
}
function UpdateBanner({ show, onApply, t }: UpdateBannerProps) {
  if (!show) return null;
  return (
    <div className="update-banner">
      <span className="update-banner-text">✨ {t.updateAvailable}</span>
      <button className="update-banner-btn" onClick={onApply}>{t.applyUpdate}</button>
    </div>
  );
}

(Object.assign as any)(window, {
  CircularTimer, SoundCard, CustomSoundCard, CustomSoundsBar,
  MasterBar, NowPlaying, PresetsRow, RecordModal, UpdateBanner,
});
