// lib/widgets/bouncing_loader.dart import 'package:flutter/material.dart'; /// Three-dot bouncing loader using Curves.bounceOut. /// Drop-in replacement for CircularProgressIndicator on full-screen loads. class BouncingLoader extends StatefulWidget { final Color? color; final double dotSize; final double spacing; const BouncingLoader({ Key? key, this.color, this.dotSize = 8.0, this.spacing = 6.0, }) : super(key: key); @override State createState() => _BouncingLoaderState(); } class _BouncingLoaderState extends State with TickerProviderStateMixin { late final List _controllers; late final List> _animations; static const _duration = Duration(milliseconds: 600); static const _staggerDelay = Duration(milliseconds: 200); @override void initState() { super.initState(); _controllers = List.generate( 3, (i) => AnimationController(vsync: this, duration: _duration), ); _animations = _controllers.map((c) { return Tween(begin: 0.0, end: -12.0).animate( CurvedAnimation(parent: c, curve: Curves.bounceOut), ); }).toList(); _startWithStagger(); } void _startWithStagger() async { for (int i = 0; i < _controllers.length; i++) { await Future.delayed(i == 0 ? Duration.zero : _staggerDelay); if (!mounted) return; _startLoop(i); } } void _startLoop(int index) { if (!mounted) return; _controllers[index].forward(from: 0).whenComplete(() { if (mounted) { Future.delayed( Duration(milliseconds: _staggerDelay.inMilliseconds * (_controllers.length - 1)), () { if (mounted) _startLoop(index); }, ); } }); } @override void dispose() { for (final c in _controllers) { c.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { final dotColor = widget.color ?? Theme.of(context).colorScheme.primary; return Row( mainAxisSize: MainAxisSize.min, children: List.generate(3, (i) { return Padding( padding: EdgeInsets.symmetric(horizontal: widget.spacing / 2), child: AnimatedBuilder( animation: _animations[i], builder: (_, __) => Transform.translate( offset: Offset(0, _animations[i].value), child: Container( width: widget.dotSize, height: widget.dotSize, decoration: BoxDecoration( color: dotColor, shape: BoxShape.circle, ), ), ), ), ); }), ); } }