// 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 */}
T
Team Sync
online
Mon 9:41
{/* 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 ? '✓' : '↑'}
); };