# Dodge Field

> Wrapper that makes any child dodge the pointer until it relents and glides home.

A field that makes its child flee the pointer. Move toward the pill and it darts away; each near miss counts a dodge and swaps the taunt. After a few dodges it gives in, sits still and lets you click it. Touch devices never dodge.

<demo-dodge-field>



</demo-dodge-field>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/dodge-field.json"
```

## Usage

```vue
<script setup lang="ts">
  import DodgeField from '~/components/ui/dodge-field/DodgeField.vue';
</script>

<template>
  <DodgeField
    :reach="72"
    :radius="120"
    :patience="4"
    notice="Nothing dodges on touch. Tap away."
    @catch="() => console.log('caught')"
  />

  <!-- Any child, with the live state in the slot -->
  <DodgeField v-slot="{ dodges, gave }" axis="x" wall="bounce">
    <button type="button" class="rounded-full border px-4 py-2">
      {{ gave ? 'Fine, subscribe' : `Subscribe (${dodges})` }}
    </button>
  </DodgeField>
</template>
```

The pointer distance from the field centre drives a flee strength of `(1 - d / radius) ^ falloff`. The child springs `reach` pixels away along the line from the pointer, then glides home with a softer spring when the pointer leaves. A dodge counts once the pointer crosses 55% of the radius, and re-arms only after it leaves the radius.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        taunts
      </code>
    </td>
    
    <td>
      <code>
        string[]
      </code>
    </td>
    
    <td>
      <code>
        ['Catch me', 'Nope', 'Too slow', 'Almost', 'Okay, okay']
      </code>
    </td>
    
    <td>
      Built-in pill labels: first at rest, one per dodge, last once it relents.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        notice
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      One line shown under the child on touch devices, where nothing dodges.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ink-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'var(--foreground)'
      </code>
    </td>
    
    <td>
      Pill tint, text, caught fill and focus ring all derive from it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        contrast-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'var(--background)'
      </code>
    </td>
    
    <td>
      Text colour while the pill is caught.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        field-height
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        240
      </code>
    </td>
    
    <td>
      Height of the invisible field in pixels; its width is the container.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        reach
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        72
      </code>
    </td>
    
    <td>
      How far it bolts, in pixels, with the pointer on its home.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        120
      </code>
    </td>
    
    <td>
      Distance from home at which it starts to react, in pixels.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        falloff
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        2
      </code>
    </td>
    
    <td>
      Reaction curve exponent. 1 drifts from far off; 4 flinches at the last moment.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        flee-duration
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        130
      </code>
    </td>
    
    <td>
      Settle time of the dart, in milliseconds.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        return-duration
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        620
      </code>
    </td>
    
    <td>
      Settle time of the glide home, in milliseconds.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        return-bounce
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.1
      </code>
    </td>
    
    <td>
      Overshoot on the way home. 0.1 is a hair, 0.3 a visible wobble.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        axis
      </code>
    </td>
    
    <td>
      <code>
        'both' | 'x' | 'y'
      </code>
    </td>
    
    <td>
      <code>
        'both'
      </code>
    </td>
    
    <td>
      Which way it may flee.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wall
      </code>
    </td>
    
    <td>
      <code>
        'clamp' | 'bounce'
      </code>
    </td>
    
    <td>
      <code>
        'clamp'
      </code>
    </td>
    
    <td>
      At the field edge: stop dead, or fold the overflow back so it rebounds.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        patience
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        4
      </code>
    </td>
    
    <td>
      Dodges before it gives in and sits still, at least 1.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        disabled
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Sits still and stops counting; the child stays clickable.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        HTMLAttributes['class']
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Extra classes for the field.
    </td>
  </tr>
</tbody>
</table>

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Payload
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        dodge
      </code>
    </td>
    
    <td>
      <code>
        (count: number)
      </code>
    </td>
    
    <td>
      Fired on each counted dodge.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        relent
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Fired when it gives in.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        catch
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Fired when the child is clicked.
    </td>
  </tr>
</tbody>
</table>

## Slots

<table>
<thead>
  <tr>
    <th>
      Slot
    </th>
    
    <th>
      Props
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      <code>
        { dodges: number, gave: boolean, caught: boolean, fleeing: boolean }
      </code>
    </td>
    
    <td>
      What flees. Omit it for the built-in taunting pill.
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The built-in pill is a real `<button>`; keyboard users reach it with Tab and press it with Enter or Space. Keyboard focus never makes it dodge.
- With `prefers-reduced-motion: reduce` it never moves, and the label crossfade drops its blur.
- Never wrap a decline, close or cancel control in a field that dodges.
- Ported from [React Bits](https://reactbits.dev/micro/dodge-field) by David Haz.
