blinc_cn 0.5.1

Blinc Component Library - shadcn-style themed components built on blinc_layout primitives
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
//! Slider component for range value selection
//!
//! A themed slider/range input with click-to-set and drag-to-adjust.
//! Uses context-driven state for proper persistence across UI rebuilds.
//!
//! # Example
//!
//! ```ignore
//! use blinc_cn::prelude::*;
//!
//! fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
//!     // Create slider state from context (0.0 to 1.0 by default)
//!     let volume = ctx.use_state_for("volume", 0.5);
//!
//!     cn::slider(ctx, &volume)
//!         .label("Volume")
//!         .on_change(|value| println!("Volume: {}", value))
//! }
//!
//! // Custom range
//! let brightness = ctx.use_state_for("brightness", 50.0);
//! cn::slider(ctx, &brightness)
//!     .min(0.0)
//!     .max(100.0)
//!     .step(1.0)
//!
//! // Different sizes
//! cn::slider(ctx, &value)
//!     .size(SliderSize::Large)
//!
//! // Custom colors
//! cn::slider(ctx, &value)
//!     .track_color(Color::GRAY)
//!     .fill_color(Color::BLUE)
//!     .thumb_color(Color::WHITE)
//!
//! // Disabled state
//! cn::slider(ctx, &value)
//!     .disabled(true)
//! ```

use blinc_animation::{get_scheduler, AnimationContext, SpringConfig};
use blinc_core::events::event_types;
use blinc_core::{BlincContext, BlincContextState, Color, State};
use blinc_layout::div::ElementTypeId;
use blinc_layout::element::{CursorStyle, RenderProps};
use blinc_layout::motion::motion;
use blinc_layout::prelude::*;
use blinc_layout::stateful::{stateful_with_key, NoState, StateTransitions};
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_macros::BlincComponent;
use blinc_theme::{ColorToken, RadiusToken, ThemeState};
use std::sync::{Arc, Mutex};

use super::label::{label, LabelSize};
use blinc_layout::InstanceKey;

/// Slider thumb interaction states
///
/// Unlike `ButtonState`, this FSM handles DRAG and DRAG_END events
/// to properly track dragging state even when mouse leaves the thumb.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SliderThumbState {
    #[default]
    Idle,
    Hovered,
    Pressed,
    Dragging,
}

impl StateTransitions for SliderThumbState {
    fn on_event(&self, event: u32) -> Option<Self> {
        match (self, event) {
            // Idle transitions
            (SliderThumbState::Idle, event_types::POINTER_ENTER) => Some(SliderThumbState::Hovered),

            // Hovered transitions
            (SliderThumbState::Hovered, event_types::POINTER_LEAVE) => Some(SliderThumbState::Idle),
            (SliderThumbState::Hovered, event_types::POINTER_DOWN) => {
                Some(SliderThumbState::Pressed)
            }

            // Pressed transitions
            (SliderThumbState::Pressed, event_types::POINTER_UP) => Some(SliderThumbState::Hovered),
            (SliderThumbState::Pressed, event_types::POINTER_LEAVE) => Some(SliderThumbState::Idle),
            // When dragging starts, transition to Dragging
            (SliderThumbState::Pressed, event_types::DRAG) => Some(SliderThumbState::Dragging),

            // Dragging transitions - stays in Dragging until DRAG_END
            (SliderThumbState::Dragging, event_types::DRAG) => None, // Stay in Dragging
            (SliderThumbState::Dragging, event_types::DRAG_END) => Some(SliderThumbState::Idle),
            // Ignore POINTER_LEAVE/ENTER while dragging - we don't want visual changes
            (SliderThumbState::Dragging, event_types::POINTER_LEAVE) => None,
            (SliderThumbState::Dragging, event_types::POINTER_ENTER) => None,
            // POINTER_UP also ends dragging (fallback if DRAG_END not fired)
            (SliderThumbState::Dragging, event_types::POINTER_UP) => Some(SliderThumbState::Idle),

            _ => None,
        }
    }
}

/// BlincComponent for slider state and animations
/// Generates type-safe hooks that persist across UI rebuilds:
/// - SliderState::use_thumb_offset(ctx, initial, config) -> SharedAnimatedValue
/// - SliderState::use_drag_start_x(ctx, 0.0) -> State<f32>
#[derive(BlincComponent)]
struct SliderState {
    /// Animated X offset for thumb position
    #[animation]
    thumb_offset: f32,
    /// Mouse X position at drag start (screen coordinates)
    drag_start_x: f32,
    /// Thumb offset at drag start
    drag_start_offset: f32,
    /// Whether a drag is currently in progress (to suppress click-to-jump)
    is_dragging: bool,
}

/// Slider size variants
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SliderSize {
    /// Small slider (track: 4px, thumb: 14px)
    Small,
    /// Medium slider (track: 6px, thumb: 18px)
    #[default]
    Medium,
    /// Large slider (track: 8px, thumb: 22px)
    Large,
}

impl SliderSize {
    /// Get the track height for this size
    fn track_height(&self) -> f32 {
        match self {
            SliderSize::Small => 4.0,
            SliderSize::Medium => 6.0,
            SliderSize::Large => 8.0,
        }
    }

    /// Get the thumb diameter for this size
    fn thumb_size(&self) -> f32 {
        match self {
            SliderSize::Small => 14.0,
            SliderSize::Medium => 18.0,
            SliderSize::Large => 22.0,
        }
    }
}

/// Slider component
///
/// A range slider with click-to-set and drag-to-adjust value.
/// Uses context-driven state for proper persistence across UI rebuilds.
pub struct Slider {
    /// The fully-built inner element (Div containing slider and optional label)
    inner: Div,
}

impl Slider {
    /// Create a new slider with context and state
    ///
    /// # Example
    /// ```ignore
    /// let volume = ctx.use_state_for("volume", 0.5);
    /// cn::slider(&volume).build_final(ctx)
    /// ```
    #[track_caller]
    pub fn new(value_state: &State<f32>) -> Self {
        Self::with_config(
            InstanceKey::new("slider"),
            SliderConfig::new(value_state.clone()),
        )
    }

    /// Create from a full configuration
    fn with_config(key: InstanceKey, config: SliderConfig) -> Self {
        let theme = ThemeState::get();
        let track_height = config.size.track_height();
        let thumb_size = config.size.thumb_size();
        let radius = theme.radius(RadiusToken::Full);

        // Get colors
        let track_bg = config
            .track_color
            .unwrap_or_else(|| theme.color(ColorToken::SurfaceElevated));
        let thumb_bg = config
            .thumb_color
            .unwrap_or_else(|| theme.color(ColorToken::Border).with_alpha(1.0));
        // Fill color for the filled portion of the track
        let fill_bg = config
            .fill_color
            .unwrap_or_else(|| theme.color(ColorToken::Primary));
        // Note: border_hover could be used for hover effects (not yet implemented due to motion transform limitation)
        let _border_hover = theme.color(ColorToken::BorderHover);

        let disabled = config.disabled;
        let min = config.min;
        let max = config.max;
        let step = config.step;
        let width: Option<f32> = config.width;

        // Track width - use config width or default
        let track_width = config.width.unwrap_or(300.0);

        // Calculate initial thumb offset based on current value
        let initial_value = config.value_state.get();
        let initial_norm = ((initial_value - min) / (max - min)).clamp(0.0, 1.0);
        let initial_offset = initial_norm * (track_width - thumb_size);

        // Get PERSISTED state from context using BlincComponent macro
        // These survive across UI rebuilds!
        // Use the instance_key from InstanceKey so each slider has its own state
        let instance_key = key.get().to_string();

        let ctx = BlincContextState::get();
        let scheduler = get_scheduler();

        let thumb_offset = Arc::new(Mutex::new(AnimatedValue::new(
            scheduler,
            initial_offset,
            SpringConfig::snappy(),
        )));
        let drag_start_x = ctx.use_state_keyed(&format!("{}_drag_start_x", instance_key), || 0.0);
        let drag_start_offset =
            ctx.use_state_keyed(&format!("{}_drag_start_offset", instance_key), || 0.0);
        let is_dragging = ctx.use_state_keyed(&format!("{}_is_dragging", instance_key), || false);

        // Clones for closures
        let thumb_offset_for_click = thumb_offset.clone();

        // Round to step helper
        let round_to_step = move |value: f32| -> f32 {
            if let Some(s) = step {
                if s > 0.0 {
                    let steps = ((value - min) / s).round();
                    (min + steps * s).clamp(min, max)
                } else {
                    value.clamp(min, max)
                }
            } else {
                value.clamp(min, max)
            }
        };
        let round_to_step_click = round_to_step;
        let round_to_step_drag = round_to_step;

        // Clones for event handlers
        let value_state_for_click = config.value_state.clone();
        let value_state_for_drag = config.value_state.clone();
        // let value_state_for_fill = config.value_state.clone();
        let on_change_for_click = config.on_change.clone();
        let on_change_for_drag = config.on_change.clone();

        // Clone for container drag handling and fill
        let thumb_offset_for_fill = thumb_offset.clone();
        let thumb_offset_for_drag = thumb_offset.clone();
        let thumb_offset_for_down = thumb_offset.clone();
        let drag_start_x_for_down = drag_start_x.clone();
        let drag_start_offset_for_down = drag_start_offset.clone();
        let drag_start_x_for_drag = drag_start_x.clone();
        let drag_start_offset_for_drag = drag_start_offset.clone();
        let is_dragging_for_click = is_dragging.clone();
        let is_dragging_for_drag = is_dragging.clone();
        let is_dragging_for_drag_end = is_dragging.clone();
        let is_dragging_for_thumb = is_dragging.clone();
        let is_dragging_for_leave = is_dragging.clone();

        // Get visual feedback colors
        let thumb_border_dragging = theme.color(ColorToken::Primary);

        // Thumb element - uses Stateful with deps on is_dragging to show visual feedback
        // Since motion.translate_x() uses visual transform, hit testing misses the thumb,
        // but we can still react to the is_dragging state signal for visual changes.
        let thumb_key = format!("{}_thumb", instance_key);
        let thumb = stateful_with_key::<NoState>(&thumb_key)
            .deps([is_dragging.signal_id()])
            .on_state(move |_ctx| {
                let dragging = is_dragging_for_thumb.get();
                let mut thumb_div = div()
                    .class("cn-slider-thumb")
                    .w(thumb_size)
                    .h(thumb_size)
                    .rounded(thumb_size / 2.0)
                    .border(2.0, theme.color(ColorToken::Border))
                    .bg(thumb_bg)
                    .shadow_sm();

                if dragging {
                    // Visual feedback when dragging: add border
                    thumb_div = thumb_div.border(2.0, thumb_border_dragging).shadow_md();
                }

                thumb_div
            });

        // Filled portion of track
        //
        // The fill bar is positioned so its right edge aligns with the thumb center.
        // Both fill and thumb share the same animated offset value, so they move together.
        //
        // Layout:
        // - A full-width fill bar starts at negative left position
        // - Motion translates it by thumb_offset (same as thumb)
        // - Result: fill right edge aligns with thumb center

        // The fill bar - full track width
        let fill_bar = div()
            .class("cn-slider-fill")
            .w(track_width)
            .h(track_height)
            .rounded(radius)
            .bg(fill_bg);

        // Position fill so its right edge is at thumb center when thumb_offset=0
        // At offset=0, fill right edge should be at thumb_size/2
        // So fill left edge should be at: thumb_size/2 - track_width
        let fill_left = thumb_size / 2.0 - track_width;
        let fill_positioned = div().absolute().left(fill_left).top(0.0).child(fill_bar);

        // Motion translates by thumb_offset - same value as thumb uses
        let animated_fill = motion()
            .translate_x(thumb_offset_for_fill.clone())
            .child(fill_positioned);

        // Container for animated fill with clipping
        let track_fill = div()
            .absolute()
            .left(0.0)
            .top((thumb_size - track_height) / 2.0)
            .w(track_width)
            .h(track_height)
            .overflow_clip()
            .rounded(radius)
            .relative() // Positioning context for absolute child
            .child(animated_fill);

        // Track visual element (the thin bar) - owns click-to-jump behavior
        // Track is absolutely positioned and centered vertically
        let track_visual = div()
            .class("cn-slider-track")
            .absolute()
            .left(0.0)
            .right(0.0)
            .top((thumb_size - track_height) / 2.0) // Center vertically
            .h(track_height)
            .rounded(radius)
            .bg(track_bg)
            .cursor_pointer()
            // Track owns the click-to-jump behavior
            // Skip if a drag just occurred (is_dragging is cleared on DRAG_END)
            .on_click(move |event| {
                if disabled {
                    return;
                }

                // Skip click-to-jump if we just finished dragging
                // (the click event fires after drag end)
                if is_dragging_for_click.get() {
                    is_dragging_for_click.set(false);
                    return;
                }

                let track_w = event.bounds_width;

                if track_w > 0.0 {
                    // Calculate normalized position from click
                    let x = event.local_x;
                    let norm = (x / track_w).clamp(0.0, 1.0);
                    let raw = min + norm * (max - min);
                    let new_val = round_to_step_click(raw);
                    value_state_for_click.set(new_val);

                    // Animate thumb to clicked position with spring
                    let x_offset = norm * (track_w - thumb_size);
                    thumb_offset_for_click.lock().unwrap().set_target(x_offset);

                    if let Some(ref cb) = on_change_for_click {
                        cb(new_val);
                    }
                }
            });

        // Thumb wrapper - absolutely positioned at left=0, top=0
        // Motion.translate_x moves it visually from this base position
        let thumb_wrapper = div()
            .absolute()
            .left(0.0)
            .top(0.0)
            .child(motion().translate_x(thumb_offset).child(thumb));

        // Build the slider using div() with relative positioning
        //
        // IMPORTANT: The container handles ALL drag events because:
        // - motion().translate_x() uses visual transform (GPU-level), not layout transform
        // - Hit testing uses layout bounds, so clicks at the thumb's visual position miss it
        // - The container spans the full track width and always receives events correctly
        let mut slider_container = div()
            .relative() // Positioning context for absolute children
            .h(thumb_size)
            .overflow_visible() // Allow thumb to overflow if needed
            .cursor(CursorStyle::Grab)
            // Track background layer (absolutely positioned, centered)
            .child(track_visual)
            // Track fill layer (shows progress, on top of background)
            .child(track_fill)
            // Thumb with motion translation for visual positioning (absolutely positioned)
            .child(thumb_wrapper)
            // Container handles POINTER_DOWN to capture drag start position
            .on_mouse_down(move |event| {
                if disabled {
                    return;
                }
                // Store mouse X position and current thumb offset at drag start
                drag_start_x_for_down.set(event.mouse_x);
                let current = thumb_offset_for_down.lock().unwrap().get();
                drag_start_offset_for_down.set(current);
            })
            // Container handles DRAG to update thumb position
            // Uses mouse_x delta from drag start to calculate new offset
            .on_drag(move |event| {
                if disabled {
                    return;
                }
                // Mark that we're dragging (to suppress click-to-jump on release)
                is_dragging_for_drag.set(true);

                // Calculate delta from drag start using absolute mouse coordinates
                let start_x = drag_start_x_for_drag.get();
                let delta_x = event.mouse_x - start_x;
                let start_offset = drag_start_offset_for_drag.get();
                let max_offset = track_width - thumb_size;
                let new_offset = (start_offset + delta_x).clamp(0.0, max_offset);

                // Update thumb position immediately (no spring animation during drag)
                thumb_offset_for_drag
                    .lock()
                    .unwrap()
                    .set_immediate(new_offset);

                // Calculate and update value
                let norm = new_offset / max_offset;
                let raw = min + norm * (max - min);
                let new_val = round_to_step_drag(raw);
                value_state_for_drag.set(new_val);

                if let Some(ref cb) = on_change_for_drag {
                    cb(new_val);
                }
            })
            // DRAG_END - keep is_dragging true so click handler can clear it
            // (click fires after drag_end, so we need the flag to persist briefly)
            .on_drag_end(move |_event| {
                // is_dragging stays true - click handler will clear it
                // This prevents click-to-jump from firing after a drag
                let _ = is_dragging_for_drag_end.get(); // keep closure alive
            })
            // Mouse leave - clear is_dragging to reset visual state
            .on_hover_leave(move |_event| {
                is_dragging_for_leave.set(false);
            });

        // Apply width
        if let Some(w) = width {
            slider_container = slider_container.w(w);
        } else {
            slider_container = slider_container.w_full();
        }

        if disabled {
            slider_container = slider_container.opacity(0.5);
        }

        // If there's a label or show_value, wrap in a container
        let inner = if config.label.is_some() || config.show_value {
            let spacing = theme.spacing_value(blinc_theme::SpacingToken::Space2);
            let mut outer = div().h_fit().flex_col().gap_px(spacing);

            // Apply width to container
            if let Some(w) = width {
                outer = outer.w(w);
            } else {
                outer = outer.w_full();
            }

            // Header row with label and optional value
            if config.label.is_some() || config.show_value {
                let mut header = div().flex_row().justify_between().items_center();

                if let Some(ref label_text) = config.label {
                    let mut lbl = label(label_text).size(LabelSize::Medium);
                    if disabled {
                        lbl = lbl.disabled(true);
                    }
                    header = header.child(lbl);
                }

                if config.show_value {
                    let value_color = if disabled {
                        theme.color(ColorToken::TextTertiary)
                    } else {
                        theme.color(ColorToken::TextSecondary)
                    };
                    let value_state_for_display = config.value_state.clone();
                    let step_for_display = config.step;

                    // Use Stateful with deps to make value text reactive
                    let value_display_key = format!("{}_value_display", instance_key);
                    let value_display = stateful_with_key::<NoState>(&value_display_key)
                        .deps([config.value_state.signal_id()])
                        .on_state(move |_ctx| {
                            let current_value = value_state_for_display.get();
                            let value_text =
                                if step_for_display.is_some() && step_for_display.unwrap() >= 1.0 {
                                    format!("{:.0}", current_value)
                                } else {
                                    format!("{:.2}", current_value)
                                };
                            div().child(text(&value_text).size(14.0).color(value_color))
                        });
                    header = header.child(value_display);
                }

                outer = outer.child(header);
            }

            outer = outer.child(slider_container);
            outer
        } else {
            // Wrap container in a div for consistent return type
            div().h_fit().child(slider_container)
        };

        Self {
            inner: div().child(inner),
        }
    }

    /// Add a CSS class for selector matching
    pub fn class(mut self, name: impl Into<String>) -> Self {
        self.inner = self.inner.class(name);
        self
    }

    /// Set the element ID for CSS selector matching
    pub fn id(mut self, id: &str) -> Self {
        self.inner = self.inner.id(id);
        self
    }
}

impl ElementBuilder for Slider {
    fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
        self.inner.build(tree)
    }

    fn render_props(&self) -> RenderProps {
        self.inner.render_props()
    }

    fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
        self.inner.children_builders()
    }

    fn element_type_id(&self) -> ElementTypeId {
        self.inner.element_type_id()
    }

    fn element_classes(&self) -> &[String] {
        self.inner.element_classes()
    }
}

/// Internal configuration for building a Slider
#[derive(Clone)]
struct SliderConfig {
    value_state: State<f32>,
    min: f32,
    max: f32,
    step: Option<f32>,
    size: SliderSize,
    label: Option<String>,
    show_value: bool,
    disabled: bool,
    width: Option<f32>,
    track_color: Option<Color>,
    fill_color: Option<Color>,
    thumb_color: Option<Color>,
    on_change: Option<Arc<dyn Fn(f32) + Send + Sync>>,
}

impl SliderConfig {
    fn new(value_state: State<f32>) -> Self {
        Self {
            value_state,
            min: 0.0,
            max: 1.0,
            step: None,
            size: SliderSize::default(),
            label: None,
            show_value: false,
            disabled: false,
            width: None,
            track_color: None,
            fill_color: None,
            thumb_color: None,
            on_change: None,
        }
    }
}

/// Builder for creating Slider components with fluent API
///
/// Unlike other builders, this one builds the slider immediately when `build_final()` is called,
/// because the context reference cannot be stored due to lifetime constraints.
pub struct SliderBuilder {
    key: InstanceKey,
    config: SliderConfig,
}

impl SliderBuilder {
    /// Create a new slider builder with value state
    ///
    /// Uses `#[track_caller]` to generate a unique instance key based on the call site.
    #[track_caller]
    pub fn new(value_state: &State<f32>) -> Self {
        Self {
            key: InstanceKey::new("slider"),
            config: SliderConfig::new(value_state.clone()),
        }
    }

    /// Create a slider builder with an explicit key
    pub fn with_key(key: impl Into<String>, value_state: &State<f32>) -> Self {
        Self {
            key: InstanceKey::explicit(key),
            config: SliderConfig::new(value_state.clone()),
        }
    }

    /// Set the minimum value (default: 0.0)
    pub fn min(mut self, min: f32) -> Self {
        self.config.min = min;
        self
    }

    /// Set the maximum value (default: 1.0)
    pub fn max(mut self, max: f32) -> Self {
        self.config.max = max;
        self
    }

    /// Set the step size for discrete values
    pub fn step(mut self, step: f32) -> Self {
        self.config.step = Some(step);
        self
    }

    /// Set the slider size
    pub fn size(mut self, size: SliderSize) -> Self {
        self.config.size = size;
        self
    }

    /// Add a label above the slider
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.config.label = Some(label.into());
        self
    }

    /// Show the current value next to the slider
    pub fn show_value(mut self) -> Self {
        self.config.show_value = true;
        self
    }

    /// Set disabled state
    pub fn disabled(mut self, disabled: bool) -> Self {
        self.config.disabled = disabled;
        self
    }

    /// Set a fixed width for the slider track
    pub fn w(mut self, width: f32) -> Self {
        self.config.width = Some(width);
        self
    }

    /// Set the unfilled track color
    pub fn track_color(mut self, color: impl Into<Color>) -> Self {
        self.config.track_color = Some(color.into());
        self
    }

    /// Set the filled portion color
    pub fn fill_color(mut self, color: impl Into<Color>) -> Self {
        self.config.fill_color = Some(color.into());
        self
    }

    /// Set the thumb color
    pub fn thumb_color(mut self, color: impl Into<Color>) -> Self {
        self.config.thumb_color = Some(color.into());
        self
    }

    /// Set the change callback
    ///
    /// Called when the slider value changes.
    pub fn on_change<F>(mut self, callback: F) -> Self
    where
        F: Fn(f32) + Send + Sync + 'static,
    {
        self.config.on_change = Some(Arc::new(callback));
        self
    }

    /// Build the final Slider component with the given context
    ///
    /// This must be called last to create the actual Slider element.
    pub fn build_final(self) -> Slider {
        Slider::with_config(self.key, self.config)
    }
}

/// Create a slider with context and state
///
/// The slider uses context-driven state that persists across UI rebuilds.
/// Uses BlincComponent macro for type-safe state management.
///
/// **Important**: Call `.build_final(ctx)` at the end of the builder chain
/// to create the final Slider element.
///
/// # Example
///
/// ```ignore
/// use blinc_cn::prelude::*;
///
/// fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder {
///     let volume = ctx.use_state_for("volume", 0.5);
///
///     cn::slider(&volume)
///         .min(0.0)
///         .max(1.0)
///         .label("Volume")
///         .show_value()
///         .on_change(|v| println!("Volume: {}", v))
///         .build_final(ctx)
/// }
/// ```
#[track_caller]
pub fn slider(state: &State<f32>) -> SliderBuilder {
    SliderBuilder::new(state)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_slider_sizes() {
        assert_eq!(SliderSize::Small.track_height(), 4.0);
        assert_eq!(SliderSize::Medium.track_height(), 6.0);
        assert_eq!(SliderSize::Large.track_height(), 8.0);
    }

    #[test]
    fn test_slider_thumb_sizes() {
        assert_eq!(SliderSize::Small.thumb_size(), 14.0);
        assert_eq!(SliderSize::Medium.thumb_size(), 18.0);
        assert_eq!(SliderSize::Large.thumb_size(), 22.0);
    }
}