import 'package:flutter/material.dart'; class TypingDots extends StatefulWidget { const TypingDots({super.key, this.size = 6, this.color, this.spacing = 4}); final double size; final double spacing; final Color? color; @override State createState() => _TypingDotsState(); } class _TypingDotsState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } double _opacity(double t, double offset) { final phase = (t + offset) % 1.0; final distance = (phase - 0.5).abs(); final ramp = 1.0 - (distance / 0.5); return 0.3 + 0.7 * ramp.clamp(0.0, 1.0); } @override Widget build(BuildContext context) { final color = widget.color ?? Theme.of(context).colorScheme.onSurfaceVariant; return AnimatedBuilder( animation: _controller, builder: (context, child) { final t = _controller.value; return Row( mainAxisSize: MainAxisSize.min, children: [ _dot(color, _opacity(t, 0.0)), SizedBox(width: widget.spacing), _dot(color, _opacity(t, 0.2)), SizedBox(width: widget.spacing), _dot(color, _opacity(t, 0.4)), ], ); }, ); } Widget _dot(Color color, double opacity) { return Opacity( opacity: opacity, child: Container( width: widget.size, height: widget.size, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), ); } }