@@ -20,7 +20,7 @@ type Spinner struct {
 
 type model struct {
 	cancel context.CancelFunc
-	anim   anim.Anim
+	anim   *anim.Anim
 }
 
 func (m model) Init() tea.Cmd { return m.anim.Init() }
@@ -37,7 +37,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 	}
 	mm, cmd := m.anim.Update(msg)
-	m.anim = mm.(anim.Anim)
+	m.anim = mm.(*anim.Anim)
 	return m, cmd
 }
 
  
  
  
    
    @@ -6,7 +6,6 @@ import (
 	"image/color"
 	"math/rand/v2"
 	"strings"
-	"sync"
 	"sync/atomic"
 	"time"
 
@@ -15,6 +14,8 @@ import (
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/lucasb-eyer/go-colorful"
+
+	"github.com/charmbracelet/crush/internal/csync"
 )
 
 const (
@@ -72,10 +73,7 @@ type animCache struct {
 	ellipsisFrames []string
 }
 
-var (
-	animCacheMutex sync.RWMutex
-	animCacheMap   = make(map[string]*animCache)
-)
+var animCacheMap = csync.NewMap[string, *animCache]()
 
 // settingsHash creates a hash key for the settings to use for caching
 func settingsHash(opts Settings) string {
@@ -105,22 +103,23 @@ const ()
 type Anim struct {
 	width            int
 	cyclingCharWidth int
-	label            []string
+	label            *csync.Slice[string]
 	labelWidth       int
 	labelColor       color.Color
 	startTime        time.Time
 	birthOffsets     []time.Duration
 	initialFrames    [][]string // frames for the initial characters
-	initialized      bool
-	cyclingFrames    [][]string // frames for the cycling characters
-	step             int        // current main frame step
-	ellipsisStep     int        // current ellipsis frame step
-	ellipsisFrames   []string   // ellipsis animation frames
+	initialized      atomic.Bool
+	cyclingFrames    [][]string           // frames for the cycling characters
+	step             atomic.Int64         // current main frame step
+	ellipsisStep     atomic.Int64         // current ellipsis frame step
+	ellipsisFrames   *csync.Slice[string] // ellipsis animation frames
 	id               int
 }
 
 // New creates a new Anim instance with the specified width and label.
-func New(opts Settings) (a Anim) {
+func New(opts Settings) *Anim {
+	a := &Anim{}
 	// Validate settings.
 	if opts.Size < 1 {
 		opts.Size = defaultNumCyclingChars
@@ -142,16 +141,14 @@ func New(opts Settings) (a Anim) {
 
 	// Check cache first
 	cacheKey := settingsHash(opts)
-	animCacheMutex.RLock()
-	cached, exists := animCacheMap[cacheKey]
-	animCacheMutex.RUnlock()
+	cached, exists := animCacheMap.Get(cacheKey)
 
 	if exists {
 		// Use cached values
 		a.width = cached.width
 		a.labelWidth = cached.labelWidth
-		a.label = cached.label
-		a.ellipsisFrames = cached.ellipsisFrames
+		a.label = csync.NewSliceFrom(cached.label)
+		a.ellipsisFrames = csync.NewSliceFrom(cached.ellipsisFrames)
 		a.initialFrames = cached.initialFrames
 		a.cyclingFrames = cached.cyclingFrames
 	} else {
@@ -228,17 +225,23 @@ func New(opts Settings) (a Anim) {
 		}
 
 		// Cache the results
+		labelSlice := make([]string, a.label.Len())
+		for i, v := range a.label.Seq2() {
+			labelSlice[i] = v
+		}
+		ellipsisSlice := make([]string, a.ellipsisFrames.Len())
+		for i, v := range a.ellipsisFrames.Seq2() {
+			ellipsisSlice[i] = v
+		}
 		cached = &animCache{
 			initialFrames:  a.initialFrames,
 			cyclingFrames:  a.cyclingFrames,
 			width:          a.width,
 			labelWidth:     a.labelWidth,
-			label:          a.label,
-			ellipsisFrames: a.ellipsisFrames,
+			label:          labelSlice,
+			ellipsisFrames: ellipsisSlice,
 		}
-		animCacheMutex.Lock()
-		animCacheMap[cacheKey] = cached
-		animCacheMutex.Unlock()
+		animCacheMap.Set(cacheKey, cached)
 	}
 
 	// Random assign a birth to each character for a stagged entrance effect.
@@ -269,28 +272,30 @@ func (a *Anim) renderLabel(label string) {
 	if a.labelWidth > 0 {
 		// Pre-render the label.
 		labelRunes := []rune(label)
-		a.label = make([]string, len(labelRunes))
-		for i := range a.label {
-			a.label[i] = lipgloss.NewStyle().
+		a.label = csync.NewSlice[string]()
+		for i := range labelRunes {
+			rendered := lipgloss.NewStyle().
 				Foreground(a.labelColor).
 				Render(string(labelRunes[i]))
+			a.label.Append(rendered)
 		}
 
 		// Pre-render the ellipsis frames which come after the label.
-		a.ellipsisFrames = make([]string, len(ellipsisFrames))
-		for i, frame := range ellipsisFrames {
-			a.ellipsisFrames[i] = lipgloss.NewStyle().
+		a.ellipsisFrames = csync.NewSlice[string]()
+		for _, frame := range ellipsisFrames {
+			rendered := lipgloss.NewStyle().
 				Foreground(a.labelColor).
 				Render(frame)
+			a.ellipsisFrames.Append(rendered)
 		}
 	} else {
-		a.label = nil
-		a.ellipsisFrames = nil
+		a.label = csync.NewSlice[string]()
+		a.ellipsisFrames = csync.NewSlice[string]()
 	}
 }
 
 // Width returns the total width of the animation.
-func (a Anim) Width() (w int) {
+func (a *Anim) Width() (w int) {
 	w = a.width
 	if a.labelWidth > 0 {
 		w += labelGapWidth + a.labelWidth
@@ -308,12 +313,12 @@ func (a Anim) Width() (w int) {
 }
 
 // Init starts the animation.
-func (a Anim) Init() tea.Cmd {
+func (a *Anim) Init() tea.Cmd {
 	return a.Step()
 }
 
 // Update processes animation steps (or not).
-func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case StepMsg:
 		if msg.id != a.id {
@@ -321,19 +326,19 @@ func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return a, nil
 		}
 
-		a.step++
-		if a.step >= len(a.cyclingFrames) {
-			a.step = 0
+		step := a.step.Add(1)
+		if int(step) >= len(a.cyclingFrames) {
+			a.step.Store(0)
 		}
 
-		if a.initialized && a.labelWidth > 0 {
+		if a.initialized.Load() && a.labelWidth > 0 {
 			// Manage the ellipsis animation.
-			a.ellipsisStep++
-			if a.ellipsisStep >= ellipsisAnimSpeed*len(ellipsisFrames) {
-				a.ellipsisStep = 0
+			ellipsisStep := a.ellipsisStep.Add(1)
+			if int(ellipsisStep) >= ellipsisAnimSpeed*len(ellipsisFrames) {
+				a.ellipsisStep.Store(0)
 			}
-		} else if !a.initialized && time.Since(a.startTime) >= maxBirthOffset {
-			a.initialized = true
+		} else if !a.initialized.Load() && time.Since(a.startTime) >= maxBirthOffset {
+			a.initialized.Store(true)
 		}
 		return a, a.Step()
 	default:
@@ -342,35 +347,41 @@ func (a Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 }
 
 // View renders the current state of the animation.
-func (a Anim) View() string {
+func (a *Anim) View() string {
 	var b strings.Builder
+	step := int(a.step.Load())
 	for i := range a.width {
 		switch {
-		case !a.initialized && i < len(a.birthOffsets) && time.Since(a.startTime) < a.birthOffsets[i]:
+		case !a.initialized.Load() && i < len(a.birthOffsets) && time.Since(a.startTime) < a.birthOffsets[i]:
 			// Birth offset not reached: render initial character.
-			b.WriteString(a.initialFrames[a.step][i])
+			b.WriteString(a.initialFrames[step][i])
 		case i < a.cyclingCharWidth:
 			// Render a cycling character.
-			b.WriteString(a.cyclingFrames[a.step][i])
+			b.WriteString(a.cyclingFrames[step][i])
 		case i == a.cyclingCharWidth:
 			// Render label gap.
 			b.WriteString(labelGap)
 		case i > a.cyclingCharWidth:
 			// Label.
-			b.WriteString(a.label[i-a.cyclingCharWidth-labelGapWidth])
+			if labelChar, ok := a.label.Get(i - a.cyclingCharWidth - labelGapWidth); ok {
+				b.WriteString(labelChar)
+			}
 		}
 	}
 	// Render animated ellipsis at the end of the label if all characters
 	// have been initialized.
-	if a.initialized && a.labelWidth > 0 {
-		b.WriteString(a.ellipsisFrames[a.ellipsisStep/ellipsisAnimSpeed])
+	if a.initialized.Load() && a.labelWidth > 0 {
+		ellipsisStep := int(a.ellipsisStep.Load())
+		if ellipsisFrame, ok := a.ellipsisFrames.Get(ellipsisStep / ellipsisAnimSpeed); ok {
+			b.WriteString(ellipsisFrame)
+		}
 	}
 
 	return b.String()
 }
 
 // Step is a command that triggers the next step in the animation.
-func (a Anim) Step() tea.Cmd {
+func (a *Anim) Step() tea.Cmd {
 	return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg {
 		return StepMsg{id: a.id}
 	})
  
  
  
    
    @@ -45,7 +45,7 @@ type messageCmp struct {
 	// Core message data and state
 	message  message.Message // The underlying message content
 	spinning bool            // Whether to show loading animation
-	anim     anim.Anim       // Animation component for loading states
+	anim     *anim.Anim      // Animation component for loading states
 
 	// Thinking viewport for displaying reasoning content
 	thinkingViewport viewport.Model
@@ -91,7 +91,7 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.spinning = m.shouldSpin()
 		if m.spinning {
 			u, cmd := m.anim.Update(msg)
-			m.anim = u.(anim.Anim)
+			m.anim = u.(*anim.Anim)
 			return m, cmd
 		}
 	}