// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// Shared status-circle affordance for timeline and modal task controls. library; import 'package:flutter/material.dart'; /// Circular completion control with shared hover feedback. class StatusCircleButton extends StatefulWidget { /// Creates a status circle button. const StatusCircleButton({ required this.color, required this.checkColor, required this.isCompleted, required this.size, required this.borderWidth, required this.iconSize, required this.semanticLabel, this.onPressed, this.circleKey, this.hoverBlurRadius = 10, super.key, }); /// Key assigned to the animated circle surface. final Key? circleKey; /// Accent color used for the border, fill, and hover glow. final Color color; /// Color used for the completed check icon. final Color checkColor; /// Whether the circle is in its completed state. final bool isCompleted; /// Square size of the rendered circle. final double size; /// Border width for the circle outline. final double borderWidth; /// Size of the completed check icon. final double iconSize; /// Semantic label used when the control is interactive. final String semanticLabel; /// Callback invoked when the circle is tapped. final VoidCallback? onPressed; /// Blur radius used for the hover shadow. final double hoverBlurRadius; /// Creates the mutable state object used by this stateful widget. /// Flutter calls this once for each mounted widget instance before lifecycle callbacks and builds begin. @override State createState() => _StatusCircleButtonState(); } /// Mutable state for [StatusCircleButton]. class _StatusCircleButtonState extends State { /// Whether the pointer is hovering this status circle. bool _isHovered = false; /// Builds the widget subtree for this component. /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data. @override Widget build(BuildContext context) { final ring = AnimatedContainer( key: widget.circleKey, width: widget.size, height: widget.size, duration: const Duration(milliseconds: 120), curve: Curves.easeOut, decoration: BoxDecoration( shape: BoxShape.circle, color: widget.isCompleted ? widget.color : _isHovered ? widget.color.withValues(alpha: 0.12) : Colors.transparent, border: Border.all(color: widget.color, width: widget.borderWidth), boxShadow: _isHovered && widget.onPressed != null ? [ BoxShadow( color: widget.color.withValues(alpha: 0.38), blurRadius: widget.hoverBlurRadius, spreadRadius: 1, ), ] : const [], ), child: widget.isCompleted ? Icon(Icons.check, color: widget.checkColor, size: widget.iconSize) : null, ); if (widget.onPressed == null) { return ring; } return Semantics( button: true, label: widget.semanticLabel, child: MouseRegion( cursor: SystemMouseCursors.click, onEnter: (_) { setState(() { _isHovered = true; }); }, onExit: (_) { setState(() { _isHovered = false; }); }, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.onPressed, child: ring, ), ), ); } }