{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-agent",
  "title": "Voice Agent",
  "description": "A voice agent call transcript streams, handles an interruption and books a table.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/voice-agent/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\nimport { loadFont } from \"@remotion/google-fonts/HankenGrotesk\";\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GradientParams } from \"@/lib/remocn/gsh/gradient-engine\";\nimport { GshGround } from \"@/lib/remocn/gsh/gsh-ground\";\nimport type { GshPresetId } from \"@/lib/remocn/gsh/presets\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: mono } = loadMono(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface VoiceToolCall {\n  /** Function name listed in the call panel, e.g. \"book_table\". */\n  name?: string;\n  /** Shown while the call runs, e.g. \"Booking table at Fernway\". */\n  pending: string;\n  /** Shown once it resolves, e.g. \"Booked table at Fernway\". */\n  done: string;\n  /** Secondary detail after a middot, e.g. \"7:30 pm, party of 4\". */\n  detail?: string;\n  /** Result tag on the right once resolved, e.g. \"FW-2841\". */\n  result?: string;\n}\n\nexport interface VoiceTurn {\n  speaker: \"agent\" | \"caller\";\n  text: string;\n  /** Word count after which the other side cuts in. The turn ends with an em dash. */\n  interruptedAfter?: number;\n  /** Tool call fired at the end of this turn. */\n  tool?: VoiceToolCall;\n}\n\nexport interface VoiceSummaryRow {\n  label: string;\n  value: string;\n  mono?: boolean;\n}\n\nexport interface VoiceAgentProps {\n  agentName?: string;\n  agentRole?: string;\n  callerName?: string;\n  callerLine?: string;\n  turns?: VoiceTurn[];\n  summaryTitle?: string;\n  summaryStatus?: string;\n  summary?: VoiceSummaryRow[];\n  /** Seconds already on the call clock when the clip starts. */\n  startSeconds?: number;\n  theme?: \"light\" | \"dark\";\n  accent?: string;\n  speed?: number;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}\n\nconst DEFAULT_TURNS: VoiceTurn[] = [\n  {\n    speaker: \"agent\",\n    text: \"Hi, this is Ava at Fernway. How can I help you tonight?\",\n  },\n  {\n    speaker: \"caller\",\n    text: \"Can I get a table for four this Saturday?\",\n  },\n  {\n    speaker: \"agent\",\n    text: \"Saturday works. I have 7:30 on the patio, or 8:15 inside if you would rather stay warm.\",\n    interruptedAfter: 12,\n  },\n  {\n    speaker: \"caller\",\n    text: \"7:30 on the patio. It’s a birthday, too.\",\n  },\n  {\n    speaker: \"agent\",\n    text: \"Lovely, booking that for you now.\",\n    tool: {\n      name: \"book_table\",\n      pending: \"Booking table at Fernway\",\n      done: \"Booked table at Fernway\",\n      detail: \"Sat · 7:30 pm · 4 guests\",\n      result: \"FW-2841\",\n    },\n  },\n  {\n    speaker: \"agent\",\n    text: \"You’re all set. Patio for four at 7:30, and I’ve added a birthday note.\",\n  },\n];\n\nconst DEFAULT_SUMMARY: VoiceSummaryRow[] = [\n  { label: \"Guest\", value: \"Maya Chen · party of 4\" },\n  { label: \"When\", value: \"Sat, Oct 26 · 7:30 pm\" },\n  { label: \"Seating\", value: \"Patio\" },\n  { label: \"Note\", value: \"Birthday, bring a candle\" },\n  { label: \"Confirmation\", value: \"FW-2841\", mono: true },\n];\n\nconst ICONS = {\n  mic: \"M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3\",\n  volume: \"M11 5 6 9H3v6h3l5 4zM15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13\",\n  grid: \"M5 5h.01M12 5h.01M19 5h.01M5 12h.01M12 12h.01M19 12h.01M5 19h.01M12 19h.01M19 19h.01\",\n  calendar:\n    \"M8 2.5v3.5M16 2.5v3.5M3.5 9.5h17M5.5 4.5h13a2 2 0 0 1 2 2v12.5a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2V6.5a2 2 0 0 1 2-2z\",\n  check: \"M20 6.5 9.5 17 4 11.5\",\n  download:\n    \"M12 3.5v11M7.5 10 12 14.5l4.5-4.5M4 16v2.5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V16\",\n  more: \"M5 12h.01M12 12h.01M19 12h.01\",\n  chevron: \"M9 6l6 6-6 6\",\n  phone:\n    \"M21 16.4v2.8a1.9 1.9 0 0 1-2.1 1.9 18.8 18.8 0 0 1-8.2-2.9 18.5 18.5 0 0 1-5.7-5.7A18.8 18.8 0 0 1 2.1 4.2 1.9 1.9 0 0 1 4 2h2.8a1.9 1.9 0 0 1 1.9 1.6c.1.9.4 1.8.7 2.7a1.9 1.9 0 0 1-.4 2L7.8 9.5a15.2 15.2 0 0 0 5.7 5.7l1.2-1.2a1.9 1.9 0 0 1 2-.4c.9.3 1.8.6 2.7.7a1.9 1.9 0 0 1 1.6 1.9z\",\n  sms: \"M4 5.5h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-9l-4.5 3.5v-3.5H4a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1z\",\n} as const;\ntype IconName = keyof typeof ICONS;\n\nconst BARS = 30;\nconst WPF = 2.4;\nconst HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst CARD = { mass: 0.8, damping: 18, stiffness: 160 };\nconst CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst MOVE = Easing.bezier(0.65, 0, 0.35, 1);\nconst CAMERA = Easing.bezier(0.87, 0, 0.13, 1);\nconst clampX = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\nconst tween = (f: number, a: number, b: number, easing = ENTER) =>\n  interpolate(f, [a, b], [0, 1], { ...clampX, easing });\n\ninterface Pal {\n  win: string;\n  side: string;\n  hair: string;\n  text: string;\n  muted: string;\n  faint: string;\n  idle: string;\n  callerBar: string;\n  green: string;\n  red: string;\n  shadow: string;\n  card: string;\n  cardShadow: string;\n  scrim: string;\n  dark: boolean;\n}\n\nfunction palette(theme: \"light\" | \"dark\"): Pal {\n  return theme === \"dark\"\n    ? {\n        win: \"#0f1211\",\n        side: \"#131716\",\n        hair: \"rgba(255,255,255,0.07)\",\n        text: \"#eceeed\",\n        muted: \"#9ba3a0\",\n        faint: \"#67706d\",\n        idle: \"rgba(255,255,255,0.12)\",\n        callerBar: \"#eceeed\",\n        green: \"#45d39a\",\n        red: \"#e5484d\",\n        shadow:\n          \"0 0 0 1px rgba(255,255,255,0.09), 0 40px 90px -20px rgba(0,0,0,0.7)\",\n        card: \"#161a19\",\n        cardShadow: \"0 0 0 1px rgba(255,255,255,0.08)\",\n        scrim: \"rgba(1,6,5,0.28)\",\n        dark: true,\n      }\n    : {\n        win: \"#ffffff\",\n        side: \"#f7f8f7\",\n        hair: \"rgba(16,24,20,0.08)\",\n        text: \"#141a17\",\n        muted: \"#5d6662\",\n        faint: \"#929a96\",\n        idle: \"rgba(16,24,20,0.12)\",\n        callerBar: \"#27302c\",\n        green: \"#13894f\",\n        red: \"#e5484d\",\n        shadow:\n          \"0 0 0 1px rgba(16,24,20,0.06), 0 2px 4px rgba(16,24,20,0.04), 0 30px 70px -20px rgba(10,60,45,0.28)\",\n        card: \"#ffffff\",\n        cardShadow:\n          \"0 0 0 1px rgba(16,24,20,0.07), 0 1px 3px rgba(16,24,20,0.05), 0 8px 20px -10px rgba(16,24,20,0.12)\",\n        scrim: \"rgba(240,246,243,0.22)\",\n        dark: false,\n      };\n}\n\nfunction Icon({\n  name,\n  color,\n  size = 18,\n  stroke = 1.75,\n  style,\n}: {\n  name: IconName;\n  color: string;\n  size?: number;\n  stroke?: number;\n  style?: React.CSSProperties;\n}) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      style={{ flexShrink: 0, display: \"block\", ...style }}\n      aria-hidden\n    >\n      <path\n        d={ICONS[name]}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={name === \"grid\" || name === \"more\" ? 3 : stroke}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction Spinner({\n  color,\n  size,\n  frame,\n}: {\n  color: string;\n  size: number;\n  frame: number;\n}) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      aria-hidden\n      style={{ display: \"block\", transform: `rotate(${frame * 18}deg)` }}\n    >\n      <circle\n        cx={12}\n        cy={12}\n        r={9}\n        fill=\"none\"\n        stroke={color}\n        strokeOpacity={0.2}\n        strokeWidth={2}\n      />\n      <path\n        d=\"M12 3a9 9 0 0 1 9 9\"\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction Lane({\n  label,\n  state,\n  lvl,\n  seed,\n  frame,\n  color,\n  pal,\n  barsH,\n}: {\n  label: string;\n  state: string;\n  lvl: number;\n  seed: number;\n  frame: number;\n  color: string;\n  pal: Pal;\n  barsH: number;\n}) {\n  const active = lvl > 0.04;\n  return (\n    <div style={{ padding: \"18px 0\" }}>\n      <div style={{ display: \"flex\", alignItems: \"baseline\", gap: 10 }}>\n        <div style={{ fontSize: 18, color: pal.text }}>{label}</div>\n        <div\n          style={{\n            marginLeft: \"auto\",\n            fontSize: 16,\n            color: active ? color : pal.faint,\n          }}\n        >\n          {state}\n        </div>\n      </div>\n      <div\n        style={{\n          marginTop: 14,\n          height: barsH,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n        }}\n      >\n        {Array.from({ length: BARS }, (_, i) => {\n          const c = (i - (BARS - 1) / 2) / (BARS / 2);\n          const shape = 0.35 + 0.65 * Math.exp(-c * c * 1.6);\n          const n = noise(seed + i, frame / 1.7 + i * 0.37);\n          const h = 4 + (barsH - 4) * lvl * shape * (0.12 + 0.88 * n ** 1.4);\n          const on = interpolate(lvl, [0, 0.12], [0, 1], clampX);\n          return (\n            <div\n              key={i}\n              style={{\n                width: 4,\n                height: h,\n                borderRadius: 2,\n                background: on > 0.5 ? color : pal.idle,\n                opacity: on > 0.5 ? 0.5 + 0.5 * shape : 1,\n              }}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nfunction Grow({ p, children }: { p: number; children: React.ReactNode }) {\n  return (\n    <div style={{ display: \"grid\", gridTemplateRows: `${p}fr` }}>\n      <div style={{ minHeight: 0, overflow: \"hidden\" }}>{children}</div>\n    </div>\n  );\n}\n\nfunction ToolRow({\n  tool,\n  start,\n  done,\n  frame,\n  pal,\n  accent,\n}: {\n  tool: VoiceToolCall;\n  start: number;\n  done: number;\n  frame: number;\n  pal: Pal;\n  accent: string;\n}) {\n  const inP = tween(frame, start, start + 10);\n  const swap = tween(frame, done, done + 8, MOVE);\n  const ok = spring({\n    frame: frame - done,\n    fps: 30,\n    config: CHIP,\n  });\n  return (\n    <div\n      style={{\n        marginTop: 10,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: 10,\n        height: 28,\n        fontSize: 16,\n        color: pal.muted,\n        whiteSpace: \"nowrap\",\n        opacity: inP,\n        transform: `translateY(${(1 - inP) * 8}px)`,\n      }}\n    >\n      <div style={{ position: \"relative\", width: 18, height: 18 }}>\n        <div style={{ position: \"absolute\", inset: 0, opacity: 1 - swap }}>\n          <Spinner color={accent} size={18} frame={frame} />\n        </div>\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            opacity: swap,\n            transform: `scale(${Math.max(0, ok)})`,\n          }}\n        >\n          <Icon name=\"check\" color={pal.green} size={18} stroke={2} />\n        </div>\n      </div>\n      <div style={{ position: \"relative\" }}>\n        <span style={{ opacity: 1 - swap }}>{tool.pending}</span>\n        <span\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            opacity: swap,\n            color: pal.text,\n          }}\n        >\n          {tool.done}\n        </span>\n      </div>\n      {tool.detail ? (\n        <span style={{ color: pal.faint }}>· {tool.detail}</span>\n      ) : null}\n      {tool.result ? (\n        <span\n          style={{\n            fontFamily: mono,\n            fontSize: 15.5,\n            color: pal.green,\n            opacity: swap,\n            transform: `translateX(${(1 - swap) * -6}px)`,\n          }}\n        >\n          {tool.result}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nfunction TurnRow({\n  p,\n  frame,\n  pal,\n  accent,\n  agentName,\n  callerName,\n  startSeconds,\n}: {\n  p: TurnPlan;\n  frame: number;\n  pal: Pal;\n  accent: string;\n  agentName: string;\n  callerName: string;\n  startSeconds: number;\n}) {\n  const agent = p.turn.speaker === \"agent\";\n  const cut = p.turn.interruptedAfter ? tween(frame, p.end - 2, p.end + 6) : 0;\n  return (\n    <div style={{ display: \"flex\", gap: 18, padding: \"11px 0\" }}>\n      <div\n        style={{\n          width: 52,\n          flexShrink: 0,\n          fontFamily: mono,\n          fontSize: 15.5,\n          lineHeight: \"26px\",\n          color: pal.faint,\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        {clock(startSeconds + p.start / 30)}\n      </div>\n      <div style={{ flex: 1, minWidth: 0 }}>\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"baseline\",\n            gap: 10,\n            fontSize: 16,\n            lineHeight: \"26px\",\n          }}\n        >\n          <span style={{ color: agent ? accent : pal.muted }}>\n            {agent ? agentName : callerName}\n          </span>\n          {p.turn.interruptedAfter && cut > 0 ? (\n            <span style={{ color: pal.faint, opacity: cut }}>interrupted</span>\n          ) : null}\n        </div>\n        <div\n          style={{\n            marginTop: 2,\n            fontSize: 19,\n            lineHeight: 1.45,\n            color: pal.text,\n            textWrap: \"pretty\",\n          }}\n        >\n          {p.words.map((w, k) => {\n            const o = tween(frame, p.wordAt[k], p.wordAt[k] + 5);\n            return (\n              <span\n                key={k}\n                style={{ opacity: 0.15 + 0.85 * o, color: pal.text }}\n              >\n                {w}\n                {k < p.words.length - 1 ? \" \" : \"\"}\n              </span>\n            );\n          })}\n        </div>\n        {p.turn.tool ? (\n          <ToolRow\n            tool={p.turn.tool}\n            start={p.toolStart}\n            done={p.toolDone}\n            frame={frame}\n            pal={pal}\n            accent={accent}\n          />\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nfunction SummaryCard({\n  rows,\n  title,\n  status,\n  at,\n  frame,\n  fps,\n  pal,\n  callerLine,\n}: {\n  rows: VoiceSummaryRow[];\n  title: string;\n  status: string;\n  at: number;\n  frame: number;\n  fps: number;\n  pal: Pal;\n  callerLine: string;\n}) {\n  const inP = spring({ frame: frame - at, fps, config: CARD });\n  return (\n    <div style={{ padding: \"8px 2px 4px\" }}>\n      <div\n        style={{\n          borderRadius: 14,\n          background: pal.card,\n          boxShadow: pal.cardShadow,\n          opacity: Math.min(1, inP * 1.6),\n          transform: `translateY(${(1 - inP) * 24}px)`,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"baseline\",\n            gap: 12,\n            padding: \"14px 20px 12px\",\n            borderBottom: `1px solid ${pal.hair}`,\n          }}\n        >\n          <span style={{ fontSize: 18, color: pal.text }}>{title}</span>\n          <span style={{ fontSize: 16, color: pal.green }}>{status}</span>\n        </div>\n        <div style={{ padding: \"8px 20px\" }}>\n          {rows.map((r, i) => {\n            const a = tween(frame, at + 4 + i * 2, at + 14 + i * 2);\n            return (\n              <div\n                key={r.label}\n                style={{\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  height: 32,\n                  fontSize: 16,\n                  opacity: a,\n                  transform: `translateY(${(1 - a) * 6}px)`,\n                }}\n              >\n                <span style={{ width: 132, color: pal.faint }}>{r.label}</span>\n                <span\n                  style={{\n                    color: pal.text,\n                    fontFamily: r.mono ? mono : undefined,\n                    fontSize: r.mono ? 15.5 : 16,\n                  }}\n                >\n                  {r.value}\n                </span>\n              </div>\n            );\n          })}\n        </div>\n        <div\n          style={{\n            padding: \"11px 20px 13px\",\n            borderTop: `1px solid ${pal.hair}`,\n            fontSize: 16,\n            color: pal.muted,\n            opacity: tween(\n              frame,\n              at + 6 + rows.length * 2,\n              at + 16 + rows.length * 2,\n            ),\n          }}\n        >\n          Confirmation texted to {callerLine}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function VoiceAgent({\n  agentName = \"Ava\",\n  agentRole = \"Fernway concierge\",\n  callerName = \"Maya Chen\",\n  callerLine = \"+1 (415) ••• 0192\",\n  turns = DEFAULT_TURNS,\n  summaryTitle = \"Call summary\",\n  summaryStatus = \"Booked\",\n  summary = DEFAULT_SUMMARY,\n  startSeconds = 41,\n  theme = \"light\",\n  accent = \"#3B5BFD\",\n  speed = 1,\n  ground,\n  groundParams,\n}: VoiceAgentProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width, height, fps } = useVideoConfig();\n  const tall = height > width;\n  const DW = tall ? 720 : 1280;\n  const DH = tall ? 1280 : 720;\n  const u = Math.min(width / DW, height / DH);\n  const pal = palette(theme);\n  const tl = plan(turns);\n  const ended = tween(frame, tl.callEnd, tl.callEnd + 8, MOVE);\n  const callSec = startSeconds + Math.min(frame, tl.callEnd) / 30;\n  const winIn = spring({ frame, fps, config: HEAVY });\n  const agentLvl = level(tl.turns, \"agent\", frame) * (1 - ended);\n  const callerLvl = level(tl.turns, \"caller\", frame) * (1 - ended);\n  const toolLive = tl.turns.find(\n    (p) => p.turn.tool && frame >= p.toolStart && frame < p.toolDone,\n  );\n  const agentState =\n    ended > 0.5\n      ? \"Ended\"\n      : agentLvl > 0.04\n        ? \"Speaking\"\n        : toolLive\n          ? \"Working\"\n          : \"Listening\";\n  const callerState =\n    ended > 0.5 ? \"Ended\" : callerLvl > 0.04 ? \"Speaking\" : \"Listening\";\n  const latency = Math.round(380 + 90 * noise(3, frame / 18));\n  const cam = interpolate(\n    frame,\n    [tl.summaryAt - 10, tl.summaryAt + 40],\n    [1, 1.025],\n    {\n      ...clampX,\n      easing: CAMERA,\n    },\n  );\n  const live = 1 - ended;\n  const endPress =\n    spring({ frame: frame - tl.callEnd + 2, fps, config: CHIP }) -\n    spring({ frame: frame - tl.callEnd - 3, fps, config: CHIP });\n\n  const L = tall\n    ? { win: { x: 56, y: 100, w: 608, h: 1080 }, sideW: 608, sideH: 352 }\n    : { win: { x: 136, y: 50, w: 1008, h: 620 }, sideW: 340, sideH: 0 };\n  const headH = 64;\n  const rise = (at: number): React.CSSProperties => {\n    const p = spring({ frame: frame - at, fps, config: CARD });\n    return {\n      opacity: Math.min(1, p * 1.6),\n      transform: `translateY(${(1 - p) * 12}px)`,\n    };\n  };\n\n  const sidePanel = (\n    <div\n      style={{\n        width: tall ? \"auto\" : L.sideW,\n        height: tall ? L.sideH : \"auto\",\n        flexShrink: 0,\n        borderRight: tall ? \"none\" : `1px solid ${pal.hair}`,\n        borderBottom: tall ? `1px solid ${pal.hair}` : \"none\",\n        background: pal.side,\n        padding: tall ? \"8px 28px 20px\" : \"12px 28px 24px\",\n        boxSizing: \"border-box\",\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div style={rise(6)}>\n        <Lane\n          label={agentName}\n          state={agentState}\n          lvl={agentLvl}\n          seed={11}\n          frame={frame}\n          color={accent}\n          pal={pal}\n          barsH={tall ? 44 : 84}\n        />\n      </div>\n      <div style={{ height: 1, background: pal.hair, ...rise(8) }} />\n      <div style={rise(10)}>\n        <Lane\n          label={callerName}\n          state={callerState}\n          lvl={callerLvl}\n          seed={47}\n          frame={frame}\n          color={pal.callerBar}\n          pal={pal}\n          barsH={tall ? 44 : 84}\n        />\n      </div>\n      {tall ? null : (\n        <div\n          style={{\n            marginTop: 8,\n            paddingTop: 12,\n            borderTop: `1px solid ${pal.hair}`,\n            ...rise(12),\n          }}\n        >\n          {[\n            [\"Caller\", callerLine],\n            [\"Latency\", ended > 0.5 ? \"—\" : `${latency} ms`],\n          ].map(([kk, v]) => (\n            <div\n              key={kk}\n              style={{\n                display: \"flex\",\n                justifyContent: \"space-between\",\n                height: 32,\n                alignItems: \"center\",\n                fontSize: 16,\n              }}\n            >\n              <span style={{ color: pal.faint }}>{kk}</span>\n              <span\n                style={{\n                  color: pal.muted,\n                  fontFamily: mono,\n                  fontSize: 15.5,\n                  fontVariantNumeric: \"tabular-nums\",\n                }}\n              >\n                {v}\n              </span>\n            </div>\n          ))}\n        </div>\n      )}\n      <div\n        style={{\n          marginTop: \"auto\",\n          display: \"flex\",\n          gap: 10,\n          ...rise(14),\n        }}\n      >\n        {([\"mic\", \"volume\"] as IconName[]).map((n) => (\n          <div\n            key={n}\n            style={{\n              width: 48,\n              height: 48,\n              borderRadius: 24,\n              background: pal.card,\n              boxShadow: pal.cardShadow,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              opacity: 1 - 0.5 * ended,\n            }}\n          >\n            <Icon name={n} color={pal.muted} size={20} />\n          </div>\n        ))}\n        <div\n          style={{\n            flex: 1,\n            height: 48,\n            borderRadius: 24,\n            background: live > 0.5 ? pal.red : pal.card,\n            boxShadow: live > 0.5 ? \"none\" : pal.cardShadow,\n            color: live > 0.5 ? \"#fff\" : pal.muted,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            gap: 10,\n            fontSize: 17,\n            transform: `scale(${1 - 0.04 * Math.max(0, endPress)})`,\n          }}\n        >\n          <Icon\n            name=\"phone\"\n            color={live > 0.5 ? \"#fff\" : pal.muted}\n            size={18}\n            style={{ transform: \"rotate(135deg)\" }}\n          />\n          {live > 0.5 ? \"End call\" : \"Call ended\"}\n        </div>\n      </div>\n    </div>\n  );\n\n  return (\n    <AbsoluteFill style={{ fontFamily, overflow: \"hidden\" }}>\n      <GshGround\n        preset={ground ?? \"kiruna\"}\n        params={\n          groundParams ??\n          (pal.dark\n            ? undefined\n            : {\n                background: \"#edf3f0\",\n                colors: [\"#ffffff\", \"#bfe9d9\", \"#7cc9ad\", \"#e2ece7\"],\n                grain: 0.05,\n              })\n        }\n        timeOffset={6}\n        timeScale={0.3}\n      />\n      <AbsoluteFill style={{ background: pal.scrim }} />\n      <div\n        style={{\n          position: \"absolute\",\n          left: (width - DW * u) / 2,\n          top: (height - DH * u) / 2,\n          width: DW,\n          height: DH,\n          transformOrigin: \"0 0\",\n          transform: `scale(${u})`,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: L.win.x,\n            top: L.win.y,\n            width: L.win.w,\n            height: L.win.h,\n            borderRadius: 18,\n            background: pal.win,\n            boxShadow: pal.shadow,\n            overflow: \"hidden\",\n            opacity: Math.min(1, winIn * 2),\n            transform: `translateY(${(1 - winIn) * 80}px) scale(${(0.96 + 0.04 * winIn) * cam})`,\n            transformOrigin: \"60% 70%\",\n            display: \"flex\",\n            flexDirection: \"column\",\n          }}\n        >\n          <div\n            style={{\n              height: headH,\n              flexShrink: 0,\n              display: \"flex\",\n              alignItems: \"center\",\n              padding: \"0 28px\",\n              borderBottom: `1px solid ${pal.hair}`,\n              gap: 14,\n            }}\n          >\n            <div\n              style={{\n                width: 32,\n                height: 32,\n                borderRadius: 16,\n                background: accent,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n              }}\n            >\n              <svg width={18} height={18} viewBox=\"0 0 24 24\" aria-hidden>\n                <path\n                  d=\"M4 10v4M8 7v10M12 4v16M16 8v8M20 10.5v3\"\n                  stroke=\"#fff\"\n                  strokeWidth={2}\n                  strokeLinecap=\"round\"\n                />\n              </svg>\n            </div>\n            <span style={{ fontSize: 18, color: pal.text }}>{agentName}</span>\n            <span style={{ fontSize: 16, color: pal.faint }}>{agentRole}</span>\n            <div\n              style={{\n                marginLeft: \"auto\",\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 10,\n                fontSize: 16,\n                color: live > 0.5 ? pal.red : pal.muted,\n              }}\n            >\n              <div\n                style={{\n                  width: 8,\n                  height: 8,\n                  borderRadius: 4,\n                  background: live > 0.5 ? pal.red : pal.faint,\n                  opacity:\n                    live > 0.5\n                      ? 0.55 + 0.45 * Math.abs(Math.cos(frame / 12))\n                      : 1,\n                }}\n              />\n              {live > 0.5 ? \"Live\" : \"Ended\"}\n              <span\n                style={{\n                  fontFamily: mono,\n                  fontSize: 16,\n                  color: pal.text,\n                  fontVariantNumeric: \"tabular-nums\",\n                  width: 52,\n                  textAlign: \"right\",\n                }}\n              >\n                {clock(callSec)}\n              </span>\n            </div>\n          </div>\n          <div\n            style={{\n              flex: 1,\n              minHeight: 0,\n              display: \"flex\",\n              flexDirection: tall ? \"column\" : \"row\",\n            }}\n          >\n            {sidePanel}\n            <div\n              style={{\n                flex: 1,\n                minHeight: 0,\n                minWidth: 0,\n                overflow: \"hidden\",\n                display: \"flex\",\n                flexDirection: \"column\",\n                justifyContent: \"flex-end\",\n                padding: \"0 32px 24px 28px\",\n                WebkitMaskImage:\n                  \"linear-gradient(to bottom, transparent 0, #000 40px)\",\n                maskImage:\n                  \"linear-gradient(to bottom, transparent 0, #000 40px)\",\n              }}\n            >\n              <div style={{ marginBottom: \"auto\", paddingTop: 14 }}>\n                {tl.turns.map((p, i) => {\n                  const g = tween(frame, p.start - 3, p.start + 7, MOVE);\n                  if (g <= 0) return null;\n                  return (\n                    <Grow key={i} p={g}>\n                      <div\n                        style={{\n                          opacity: tween(frame, p.start - 1, p.start + 7),\n                        }}\n                      >\n                        <TurnRow\n                          p={p}\n                          frame={frame}\n                          pal={pal}\n                          accent={accent}\n                          agentName={agentName}\n                          callerName={callerName}\n                          startSeconds={startSeconds}\n                        />\n                      </div>\n                    </Grow>\n                  );\n                })}\n                {frame >= tl.callEnd ? (\n                  <Grow p={tween(frame, tl.callEnd, tl.callEnd + 8, MOVE)}>\n                    <div\n                      style={{\n                        display: \"flex\",\n                        alignItems: \"center\",\n                        gap: 14,\n                        padding: \"14px 0 10px\",\n                        fontSize: 16,\n                        color: pal.faint,\n                        fontVariantNumeric: \"tabular-nums\",\n                      }}\n                    >\n                      <div\n                        style={{ flex: 1, height: 1, background: pal.hair }}\n                      />\n                      Call ended · {clock(callSec)}\n                      <div\n                        style={{ flex: 1, height: 1, background: pal.hair }}\n                      />\n                    </div>\n                  </Grow>\n                ) : null}\n                {frame >= tl.summaryAt - 2 ? (\n                  <Grow\n                    p={tween(frame, tl.summaryAt - 2, tl.summaryAt + 10, MOVE)}\n                  >\n                    <SummaryCard\n                      rows={summary}\n                      title={summaryTitle}\n                      status={summaryStatus}\n                      at={tl.summaryAt}\n                      frame={frame}\n                      fps={fps}\n                      pal={pal}\n                      callerLine={callerLine}\n                    />\n                  </Grow>\n                ) : null}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n\nfunction hash(n: number) {\n  const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\nfunction noise(seed: number, t: number) {\n  const i = Math.floor(t);\n  const f = t - i;\n  const k = f * f * (3 - 2 * f);\n  return hash(seed * 57 + i) * (1 - k) + hash(seed * 57 + i + 1) * k;\n}\n\nconst clock = (s: number) =>\n  `${String(Math.floor(s / 60)).padStart(2, \"0\")}:${String(Math.floor(s % 60)).padStart(2, \"0\")}`;\n\ninterface TurnPlan {\n  turn: VoiceTurn;\n  words: string[];\n  wordAt: number[];\n  start: number;\n  end: number;\n  toolStart: number;\n  toolDone: number;\n}\n\nfunction plan(turns: VoiceTurn[]) {\n  let t = 12;\n  const out: TurnPlan[] = [];\n  for (const turn of turns) {\n    const all = turn.text.split(\" \");\n    const n = Math.min(all.length, turn.interruptedAfter ?? all.length);\n    const words = all.slice(0, n);\n    if (turn.interruptedAfter) {\n      words[n - 1] = `${words[n - 1].replace(/[,.]$/, \"\")}—`;\n    }\n    const wordAt: number[] = [];\n    let w = t;\n    for (const word of words) {\n      wordAt.push(w);\n      w += WPF + (/[,.?!]$/.test(word) ? 3 : 0);\n    }\n    const end = w + 2;\n    let toolStart = -1;\n    let toolDone = -1;\n    let next = end + 9;\n    if (turn.tool) {\n      toolStart = end + 3;\n      toolDone = toolStart + 30;\n      next = toolDone + 10;\n    } else if (turn.interruptedAfter) {\n      next = end - 5;\n    }\n    out.push({ turn, words, wordAt, start: t, end, toolStart, toolDone });\n    t = next;\n  }\n  const callEnd = (out[out.length - 1]?.end ?? 0) + 12;\n  return { turns: out, callEnd, summaryAt: callEnd + 10 };\n}\n\n/** Total frames of motion for a set of turns, so a shelf can size the clip. */\nexport function voiceAgentDuration(turns: VoiceTurn[] = DEFAULT_TURNS) {\n  return Math.ceil(plan(turns).summaryAt + 40);\n}\n\nfunction level(turns: TurnPlan[], speaker: VoiceTurn[\"speaker\"], f: number) {\n  let v = 0;\n  for (const p of turns) {\n    if (p.turn.speaker !== speaker) continue;\n    const on =\n      tween(f, p.start - 2, p.start + 2) *\n      (1 - tween(f, p.end - 3, p.end + 2, Easing.bezier(0.22, 1, 0.36, 1)));\n    if (on <= 0) continue;\n    let syl = 0;\n    for (const at of p.wordAt) {\n      const d = (f - at - 1) / 1.6;\n      syl = Math.max(syl, Math.exp(-d * d));\n    }\n    v = Math.max(v, on * (0.42 + 0.58 * syl) * (0.75 + 0.25 * noise(9, f / 3)));\n  }\n  return v;\n}\n",
      "type": "registry:component",
      "target": "components/remocn/voice-agent.tsx"
    }
  ],
  "type": "registry:component"
}
