// chat-typing-reply —— App chat: typing → thinking → reply (mobile / interaction)
// Phone fades in → user types character by character in the input box (caret blinks) → on send the user bubble pops out →
// AI "thinking" dots cycle and blink → AI reply reveals character by character (with its own caret) → after settling, the whole phone breathes on hold.
// Screen is a vector mockup (dark chat theme), deterministic rendering, no randomness.
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { Phone } from '../../_fixtures/PhoneFixtures';
const CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;
export const CHAT_TYPING_REPLY_DURATION = 210; // 7s @ 30fps
// Timeline (210f)
const PHONE_IN = 4; // phone fades in
const HISTORY = 30; // chat history (user line + AI line) in place
const TYPE_START = 34; // user starts typing
const TYPE_END = 76; // typing done
const SEND = 84; // send: user bubble pops out
const THINK_START = 96; // AI thinking (three dots cycling)
const THINK_END = 138; // thinking ends, reply begins
const REPLY_START = 144; // AI reply reveals character by character
const REPLY_END = 186; // reply done
const HOLD = 192; // all settled → breathing
const USER_TEXT = 'How much time did the team work this week?';
const AI_TEXT = 'The team logged 142 hours total across 8 members this week.';
const ThinkingDots: React.FC<{ f: number }> = ({ f }) => {
const cycle = (f - THINK_START) % 18;
const op = (lead: number) =>
interpolate(
cycle,
[0, 4, 8, 12, 18],
[0.35, lead === 0 ? 1 : 0.35, lead === 1 ? 1 : 0.35, lead === 2 ? 1 : 0.35, 0.35],
CL,
);
return (
{[0, 1, 2].map((i) => (
))}
);
};
export const ChatTypingReply: React.FC = () => {
const f = useCurrentFrame();
// Phone fades in + lightly settles
const phoneIn = interpolate(f, [PHONE_IN, PHONE_IN + 16], [0.85, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
const phoneY = (1 - phoneIn) * 20;
// History messages stagger in (f8–24)
const histIn = (i: number) =>
interpolate(f, [HISTORY - 22 + i * 7, HISTORY - 12 + i * 7], [0, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
// User typing: character by character in the input + caret
const chars = Math.floor(interpolate(f, [TYPE_START, TYPE_END], [0, USER_TEXT.length], CL));
const typed = USER_TEXT.slice(0, chars);
const caretOn = f >= TYPE_START && f < SEND && Math.sin(f * 0.6) > 0;
// Send: user bubble pops out
const userBubble = interpolate(f, [SEND, SEND + 10], [0, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
const sendGlow = interpolate(f, [SEND, SEND + 3, SEND + 6], [0, 1, 0], CL);
// AI thinking indicator
const showThinking = f >= THINK_START && f < REPLY_START;
const thinkingIn = interpolate(f, [THINK_START, THINK_START + 6], [0, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
const thinkingOut = interpolate(f, [REPLY_START - 4, REPLY_START], [1, 0], CL);
// AI reply reveals character by character
const replyChars = Math.floor(
interpolate(f, [REPLY_START, REPLY_END], [0, AI_TEXT.length], CL),
);
const replyShown = AI_TEXT.slice(0, replyChars);
const replyIn = interpolate(f, [REPLY_START, REPLY_START + 8], [0, 1], {
easing: Easing.out(Easing.cubic),
...CL,
});
const replyCaret = f >= REPLY_START && f < REPLY_END && Math.sin(f * 0.6) > 0;
// After settling, the whole phone breathes
const breathe = f < HOLD ? 1 : 1 + 0.008 * Math.sin(((f - HOLD) / 48) * Math.PI * 2);
return (
{/* Chat header */}
{/* Message list */}
{/* History: user line */}
Can we review the week's hours?
{/* History: AI line */}
Sure — ask away.
{/* New user message */}
{f >= SEND && (
{USER_TEXT}
)}
{/* AI thinking */}
{showThinking &&
}
{/* AI reply reveals character by character */}
{f >= REPLY_START && (
{replyShown}
{replyCaret && (
)}
)}
{/* Input bar */}
{f >= TYPE_START && f < SEND ? typed : ''}
{caretOn && (
)}
= SEND ? '#0f7e5a' : '#1c6fe0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 18,
}}
>
{f >= SEND ? '✓' : '↑'}
);
};