I'd recommend taking a look at how this is handled in other component-based engines (not necessarily full ECS). That will give you some examples of how to attack this problem with reusable components, in a way that you know has successfully shipped other games.
Taking Unity for example, they separate out a reusable Animator component (/system) that just generically plays animation state machines provided to it as data.
The state machine (called an Animator Control Graph) is a collection of Animation Clips (timelines of keyframes & interpolation curves) which serve as nodes/states in the graph, and transitions which describe when to switch/blend between two clips, basically directed edges between nodes. It can also contain a list of properties to use in conditions that enable/disable various transitions or modulate the playback and blending.
An instance of this graph will have variables to keep track of the current state / transition, the playhead position in any playing clips, and something like a map containing the current values of its properties.
This helps decouple the visual/animated representation of a state from the player control / AI behaviour that triggers those states.
A movement component might get a reference to its Animator and send it data like:
SetFloat("horizontalSpeed", velocity.x)
SetBool("isGrounded", groundRaycast.hit)
...and the decision about how to translate those properties to animations lives completely in the data of the graph being navigated by a general-purpose animation system, rather than needing one-off code.
I could use the same movement and animation components/systems and their underlying code for a character that walks and a character that slithers, just by putting walking keyframe sequences into one graph and slithering keyframe sequences into the other, but using the same vocabulary of property keys to drive both state machines.
This pattern of "put the differences in data" repeats for the AI behaviour side of the equation too. I could make a movement component take parameters that describe its movement speed and acceleration to get many different movement behaviours out of one set of code. For their AI brains, I could use a reusable BehaviourTree component, and feed it different trees of behaviour states as data.
But to get the difference between flying enemies like bats and ground-based enemies like zombies, you'd probably want different code. To avoid creating new components/systems for every single object type, try to focus on composition: build up a complex behaviour out of reusable modular pieces.
Have a Ground Movement component and a Flying Movement component that can both expose a similar API to the brain logic in the behaviour component(s), and both communicate with the animation component. That way you can mix and match those components freely to get new behaviour combinations out of the same code (like a Vampire enemy that has both a FlyingMovement and GroundMovement, with only one enabled at a time as it switches between bat and humanoid forms...)
Overall, whenever you can, try to rephrase your problems from "is a" to "has a". It's not that a bat enemy "is a" bat. It "has a" flying animation, and a flying movement, etc. Those "has a" relationships clarify how you can represent the particular traits of a game entity with swappable data and reusable building blocks, rather than one-off code.