Three visibility states, one boolean.count > 0 shows a number, dot shows a plain circle when there's no meaningful count, and anything else renders nothing, the badge is fully unmounted, not just hidden with opacity 0, so it never eats a touch or shows up in the layout.
Entrance/exit vs. count bump are two different animations. An effect keyed on visibility and count diffs against a ref to tell them apart: hidden to shown plays the full pop (scale 0.5 to 1, opacity 0 to 1), shown to hidden reverses it and only unmounts once the fade actually finishes, and shown-to-shown-with-a-new-count skips the pop entirely and instead runs a quick scale bump (1 to 1.15 to 1), so an incrementing counter feels alive without a jarring re-pop on every tick.
withTiming only, never withSpring. A badge's count can change often, e.g. chat unread counts or notification feeds, and a spring bounce that's charming once gets old by the tenth increment.
The badge stays mounted through its own exit. Unmounting the moment the count hits 0 would kill the fade-out mid-flight, so the rendered state only flips to false inside the animation's finished callback.