# EmptyFishing

> A fishing line dropped into a quiet pond: the bobber bobs and sends out rings while two fish glide right past it under the surface. Reeds and cattails sway at the edge and a lily flowers on its pad. For "no results" and empty search screens.

- Page: https://www.boatui.dev/empty-states/fishing
- Source: https://github.com/wi11s/boatui/blob/main/components/empty-states/empty-fishing.tsx
- Animated. Generate: ~1,402 tokens (source size, single pass). Import: ~19 tokens.

## Install

All empty states: `npx degit wi11s/boatui/components/empty-states components/empty-states`

Only this one: copy the shared files (`empty-state.tsx`, `empty-state.module.css`) and `empty-fishing.tsx` and `empty-fishing.module.css` into `components/empty-states/`. Shared files are in https://www.boatui.dev/llms-full.txt.

## Usage

```tsx
import { EmptyFishing } from '@/components/empty-states/empty-fishing';

export function NoProjects() {
  return (
    <EmptyFishing>
      <h2>No projects yet</h2>
      <p>Create one to get started.</p>
    </EmptyFishing>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<EmptyFishingTheme>` | — | Colour overrides. Keys listed under theme tokens. |
| `size` | `number` | 240 | Illustration width in pixels. Scales down to fit narrow containers. |
| `paused` | `boolean` | false | Freezes the animation. |
| `className` | `string` | — | Applied to the root element (a centred column). |
| `style` | `CSSProperties` | — | Merged into the root element style, after theme variables. |
| `children` | `ReactNode` | — | Your message and actions, shown under the illustration. |

## Theme tokens

| Key | CSS variable | Default |
| --- | --- | --- |
| `water` | `--water` | `#9cc9e3` |
| `waterDeep` | `--water-deep` | `#7fb2d2` |
| `shore` | `--shore` | `#b9dca8` |
| `reeds` | `--reeds` | `#6cbf6a` |
| `cattail` | `--cattail` | `#9a6b45` |
| `rod` | `--rod` | `#9a6b45` |
| `line` | `--line` | `#8a97a0` |
| `bobber` | `--bobber` | `#e4573d` |
| `lily` | `--lily` | `#6cbf6a` |
| `flower` | `--flower` | `#f7b7c8` |
| `fish` | `--fish` | `#5d97b8` |

## Source

`components/empty-states/empty-fishing.tsx`

```tsx
// <EmptyFishing>: a fishing line dropped in a quiet pond. The bobber bobs and sends out rings while
// fish glide right past it. For "no results" and empty search screens. Animated.

import { useId } from 'react';
import { EmptyStateFrame, type EmptyStateProps } from './empty-state';
import styles from './empty-fishing.module.css';

export const emptyFishingTheme = {
  water: '#9cc9e3',
  waterDeep: '#7fb2d2',
  shore: '#b9dca8',
  reeds: '#6cbf6a',
  cattail: '#9a6b45',
  rod: '#9a6b45',
  line: '#8a97a0',
  bobber: '#e4573d',
  lily: '#6cbf6a',
  flower: '#f7b7c8',
  fish: '#5d97b8',
};
export type EmptyFishingTheme = typeof emptyFishingTheme;

/** A fishing line in a quiet pond, fish swimming past the bobber: for "no results". */
export function EmptyFishing(props: EmptyStateProps<EmptyFishingTheme>) {
  const pond = `qs${useId().replace(/[^a-zA-Z0-9_-]/g, '')}pond`;
  return (
    <EmptyStateFrame
      {...props}
      defaultTheme={emptyFishingTheme}
      art={
        <>
          <defs>
            <clipPath id={pond}>
              <ellipse cx="124" cy="134" rx="98" ry="32" />
            </clipPath>
          </defs>

          {/* Pond on a grassy bank */}
          <ellipse cx="124" cy="137" rx="110" ry="38" fill="var(--shore)" />
          <ellipse cx="124" cy="134" rx="98" ry="32" fill="var(--water)" />

          {/* Fish glide under the surface, ignoring the hook */}
          <g clipPath={`url(#${pond})`} fill="var(--fish)" opacity="0.55">
            <g className={styles.fish}>
              <g className={styles.wag}>
                <path d="M0 0 C6 -6 16 -6 22 0 C16 6 6 6 0 0 Z M0 0 L-8 -6 L-6 0 L-8 6 Z" />
              </g>
            </g>
            <g className={`${styles.fish} ${styles.fishBack}`}>
              <g className={styles.wag}>
                <path d="M0 0 C5 -4.5 12 -4.5 17 0 C12 4.5 5 4.5 0 0 Z M0 0 L-6 -4.5 L-4.5 0 L-6 4.5 Z" />
              </g>
            </g>
            <ellipse cx="124" cy="152" rx="60" ry="8" fill="var(--water-deep)" opacity="0.5" />
          </g>

          {/* Lily pad and flower */}
          <path d="M44 132 a16 6 0 1 0 14 -5 L50 132 Z" fill="var(--lily)" />
          <g fill="var(--flower)">
            <ellipse cx="54" cy="128" rx="3" ry="5" transform="rotate(-30 54 131)" />
            <ellipse cx="54" cy="128" rx="3" ry="5" transform="rotate(30 54 131)" />
            <ellipse cx="54" cy="127" rx="3" ry="5" />
          </g>

          {/* Rod from the top left, line down to the bobber */}
          <path d="M6 58 L66 18" stroke="var(--rod)" strokeWidth="4" strokeLinecap="round" />
          <circle cx="20" cy="49" r="4" fill="none" stroke="var(--rod)" strokeWidth="2" />
          <path d="M66 18 Q108 30 132 115" fill="none" stroke="var(--line)" strokeWidth="1.2" />

          {/* Rings spreading from the bobber */}
          <g fill="none" stroke="#ffffff" strokeWidth="1.6">
            {[0, 1, 2].map(i => (
              <ellipse key={i} className={styles.ring} style={{ animationDelay: `${-i * 1.1}s` }} cx="132" cy="128" rx="12" ry="3.5" />
            ))}
          </g>

          <g className={styles.bob}>
            <path d="M132 114 V120" stroke="var(--line)" strokeWidth="1.6" strokeLinecap="round" />
            <path d="M124.5 127 a7.5 7.5 0 0 1 15 0 Z" fill="#ffffff" />
            <path d="M124.5 127 a7.5 6 0 0 0 15 0 Z" fill="var(--bobber)" />
            <path d="M124.5 127 H139.5" stroke="var(--bobber)" strokeWidth="1" />
          </g>

          {/* Reeds and cattails swaying at the right */}
          <g className={styles.reeds}>
            <g fill="none" stroke="var(--reeds)" strokeWidth="2.6" strokeLinecap="round">
              <path d="M206 140 Q204 118 208 92" />
              <path d="M214 141 Q216 120 214 102" />
              <path d="M199 141 Q194 124 192 110" />
              <path d="M220 140 Q226 126 230 118" />
            </g>
            <rect x="204.5" y="90" width="6" height="16" rx="3" fill="var(--cattail)" />
            <rect x="211" y="100" width="5.5" height="13" rx="2.75" fill="var(--cattail)" />
          </g>
        </>
      }
    />
  );
}
```

`components/empty-states/empty-fishing.module.css`

```css
/* The bobber bobs, rings spread from it, reeds sway, and fish swim right past. */
.bob {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: bob 2.2s ease-in-out infinite alternate;
}

.ring {
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
  animation: ring 3.3s ease-out infinite;
}

.fish { animation: swim 11s linear infinite; }
.fishBack { animation-name: swimBack; animation-duration: 15s; animation-delay: -6s; }
.wag {
  transform-box: fill-box;
  transform-origin: 30% 50%;
  animation: wag 0.6s ease-in-out infinite alternate;
}

.reeds {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: sway 4s ease-in-out infinite alternate;
}

@keyframes bob {
  from { transform: translateY(-1.5px) rotate(-3deg); }
  to   { transform: translateY(1.5px) rotate(3deg); }
}
@keyframes ring {
  0%   { transform: scale(0.4); opacity: 0; }
  15%  { opacity: 0.8; }
  100% { transform: scale(2.6); opacity: 0; }
}
/* Positions are absolute: the fish path sits at the origin. */
@keyframes swim {
  from { transform: translate(10px, 140px); }
  to   { transform: translate(250px, 146px); }
}
@keyframes swimBack {
  from { transform: translate(240px, 152px) scaleX(-1); }
  to   { transform: translate(0px, 148px) scaleX(-1); }
}
@keyframes wag {
  from { transform: skewY(-6deg); }
  to   { transform: skewY(6deg); }
}
@keyframes sway {
  from { transform: rotate(-2deg); }
  to   { transform: rotate(2.5deg); }
}
```
