import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { Box } from '@anthropic/ink'; import { getInitialSettings } from '../../utils/settings/settings.js'; import { Clawd, type ClawdPose } from './Clawd.js'; type Frame = { pose: ClawdPose; offset: number }; /** Hold a pose for n frames (60ms each). */ function hold(pose: ClawdPose, offset: number, frames: number): Frame[] { return Array.from({ length: frames }, () => ({ pose, offset })); } // Offset semantics: marginTop in a fixed-height-3 container. 0 = normal, // 1 = crouched. Container height stays 3 so the layout never shifts; during // a crouch (offset=1) Clawd's feet row dips below the container and gets // clipped — reads as "ducking below the frame" before springing back up. // Click animation: crouch, then spring up with both arms raised. Twice. const JUMP_WAVE: readonly Frame[] = [ ...hold('default', 1, 2), // crouch ...hold('arms-up', 0, 3), // spring! ...hold('default', 0, 1), ...hold('default', 1, 2), // crouch again ...hold('arms-up', 0, 3), // spring! ...hold('default', 0, 1), ]; // Click animation: glance right, then left, then back. const LOOK_AROUND: readonly Frame[] = [ ...hold('look-right', 0, 5), ...hold('look-left', 0, 5), ...hold('default', 0, 1), ]; const CLICK_ANIMATIONS: readonly (readonly Frame[])[] = [JUMP_WAVE, LOOK_AROUND]; const IDLE: Frame = { pose: 'default', offset: 0 }; const FRAME_MS = 60; const incrementFrame = (i: number) => i + 1; const CLAWD_HEIGHT = 3; /** * Clawd with click-triggered animations (crouch-jump with arms up, or * look-around). Container height is fixed at CLAWD_HEIGHT — same footprint * as a bare `` — so the surrounding layout never shifts. During a * crouch only the feet row clips (see comment above). Click only fires when * mouse tracking is enabled (i.e. inside `` / fullscreen); * elsewhere this renders and behaves identically to plain ``. */ export function AnimatedClawd(): React.ReactNode { const { pose, bounceOffset, onClick } = useClawdAnimation(); return ( ); } function useClawdAnimation(): { pose: ClawdPose; bounceOffset: number; onClick: () => void; } { // Read once at mount — no useSettings() subscription, since that would // re-render on any settings change. const [reducedMotion] = useState(() => getInitialSettings().prefersReducedMotion ?? false); const [frameIndex, setFrameIndex] = useState(-1); const sequenceRef = useRef(JUMP_WAVE); const onClick = () => { if (reducedMotion || frameIndex !== -1) return; sequenceRef.current = CLICK_ANIMATIONS[Math.floor(Math.random() * CLICK_ANIMATIONS.length)]!; setFrameIndex(0); }; useEffect(() => { if (frameIndex === -1) return; if (frameIndex >= sequenceRef.current.length) { setFrameIndex(-1); return; } const timer = setTimeout(setFrameIndex, FRAME_MS, incrementFrame); return () => clearTimeout(timer); }, [frameIndex]); const seq = sequenceRef.current; const current = frameIndex >= 0 && frameIndex < seq.length ? seq[frameIndex]! : IDLE; return { pose: current.pose, bounceOffset: current.offset, onClick }; }