README.md

# Enguia

[![Hex.pm](https://img.shields.io/hexpm/v/enguia.svg)](https://hex.pm/packages/enguia)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/enguia)

**Declarative animations for Phoenix LiveView, powered by the Web Animations API.**

Enguia lets you add smooth, performant animations to your LiveView templates using a clean Elixir DSL. No CSS files. No JavaScript boilerplate. Just functions.

```elixir
<.motion animate={slide_up(delay: 100)} tag="section">
  <h1>Slides in when scrolled into view</h1>
</.motion>

<.motion animate={typewriter()} tag="p">
  Hello, world!
</.motion>
```

## Features

- **11 motion presets** — fade, slide, scale, shake, pulse, bounce
- **4 text effects** — typewriter, split words, blur in, letter spacing in
- **4 trigger modes** — `mount`, `visible` (scroll), `hover`, `click`
- **Scroll reveal** — re-animate every time an element enters the viewport
- **Fully composable** — override duration, delay, easing, fill, repeat per call
- **~3 KB JavaScript hook**, zero runtime dependencies

## Installation

Add `enguia` to your `mix.exs`:

```elixir
def deps do
  [
    {:enguia, "~> 0.1"}
  ]
end
```

## JavaScript Setup

In `assets/js/app.js`, import and register the hook:

```javascript
import EnguiaHook from "../../deps/enguia/priv/static/enguia.js"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { EnguiaHook }
})
```

## Usage

Import everything with `use Enguia` in your LiveView or component module:

```elixir
defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view
  use Enguia

  def render(assigns) do
    ~H"""
    <.motion animate={fade_in()}>
      <p>Fades in on mount</p>
    </.motion>

    <.motion animate={slide_up(delay: 100)} tag="section" class="hero">
      <h1>Slides up when scrolled into view</h1>
    </.motion>

    <.motion animate={bounce()} tag="button">
      Bouncing button
    </.motion>
    """
  end
end
```

Or import selectively:

```elixir
import Enguia.Components
import Enguia.Presets
```

## Motion Presets

All presets accept an optional keyword list to override defaults.

| Function | Default trigger | Description |
|---|---|---|
| `fade_in/1` | `:mount` | Fade from 0 to 1 opacity |
| `fade_out/1` | `:mount` | Fade from 1 to 0 opacity |
| `slide_up/1` | `:visible` | Slide up from below |
| `slide_down/1` | `:visible` | Slide down from above |
| `slide_left/1` | `:visible` | Slide in from the left |
| `slide_right/1` | `:visible` | Slide in from the right |
| `scale_in/1` | `:mount` | Scale from 80% to 100% |
| `scale_out/1` | `:mount` | Scale from 100% to 80% |
| `shake/1` | `:mount` | Horizontal shake (attention grabber) |
| `pulse/1` | `:mount` | Infinite opacity pulse |
| `bounce/1` | `:mount` | Infinite vertical bounce |

```elixir
fade_in(duration: 500, delay: 100, easing: "ease-out")
slide_up(trigger: :click, repeat: 2)
pulse(repeat: :infinity)   # default for pulse
```

## Text Effects

Import from `Enguia.TextAnimations` (included via `use Enguia`):

| Function | Description |
|---|---|
| `typewriter/1` | Reveals text character by character |
| `split_words/1` | Animates each word in with a stagger |
| `blur_in/1` | Fades text in from blurred to sharp |
| `letter_spacing_in/1` | Animates from wide letter-spacing to normal |

```elixir
<.motion animate={typewriter(duration: 1500)} tag="p">
  One character at a time.
</.motion>

<.motion animate={split_words(stagger: 60)} tag="h2">
  One word at a time.
</.motion>

<.motion animate={blur_in()} tag="h1">
  Fades in from blur.
</.motion>

<.motion animate={letter_spacing_in()} tag="h1">
  Tracks in from wide spacing.
</.motion>
```

## Triggers

| Trigger | When it fires |
|---|---|
| `:mount` | Immediately when the component mounts |
| `:visible` | When the element enters the viewport (IntersectionObserver) |
| `:hover` | On mouse enter |
| `:click` | On click |

### Scroll Reveal

By default, `:visible` animations fire once. Pass `scroll_reveal: true` to re-animate every time the element enters the viewport:

```elixir
<.motion animate={slide_up(scroll_reveal: true)}>
  Animates each time you scroll past it.
</.motion>
```

## Options Reference

All presets accept these keyword options:

| Option | Type | Description |
|---|---|---|
| `duration` | integer | Duration in milliseconds |
| `delay` | integer | Delay before starting, in milliseconds |
| `easing` | string | Any CSS easing value (`"ease"`, `"ease-out"`, `"linear"`, etc.) |
| `fill` | string | Fill mode: `"forwards"`, `"backwards"`, `"both"`, `"none"` |
| `trigger` | atom | `:mount`, `:visible`, `:hover`, or `:click` |
| `repeat` | integer or `:infinity` | Number of iterations |
| `scroll_reveal` | boolean | Re-animate on each viewport entry (`:visible` only) |

## Custom Animations

Build animations from scratch using `%Enguia.Animation{}`:

```elixir
alias Enguia.Animation

anim = %Animation{
  keyframes: [
    %{"transform" => "rotate(0deg)"},
    %{"transform" => "rotate(360deg)"}
  ],
  duration: 1000,
  easing: "linear",
  repeat: :infinity,
  trigger: :mount
}

<.motion animate={anim}>...</.motion>
```

## The `<.motion>` Component

| Attribute | Type | Default | Description |
|---|---|---|---|
| `animate` | `Animation.t()` | required | The animation struct |
| `tag` | string | `"div"` | HTML tag to render |
| `class` | string | `nil` | CSS class |
| `id` | string | auto-generated | Element ID |

Any other attributes are passed through to the element.

## License

MIT — see [LICENSE](LICENSE) for details.