# EmptyTelescope

> A telescope on a hilltop slowly sweeping a round patch of night sky. Stars twinkle on their own beats, the moon hangs to one side, and every 6 seconds a shooting star streaks across. For "no notifications", "nothing new" and empty activity feeds.

- Page: https://www.boatui.dev/empty-states/telescope
- Source: https://github.com/wi11s/boatui/blob/main/components/empty-states/empty-telescope.tsx
- Animated. Generate: ~1,126 tokens (source size, single pass). Import: ~20 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-telescope.tsx` and `empty-telescope.module.css` into `components/empty-states/`. Shared files are in https://www.boatui.dev/llms-full.txt.

## Usage

```tsx
import { EmptyTelescope } from '@/components/empty-states/empty-telescope';

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

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<EmptyTelescopeTheme>` | — | 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 |
| --- | --- | --- |
| `night` | `--night` | `#2a3566` |
| `stars` | `--stars` | `#fff6d8` |
| `moon` | `--moon` | `#f4e7c4` |
| `hill` | `--hill` | `#8cc66a` |
| `hillShade` | `--hill-shade` | `#74b35a` |
| `tube` | `--tube` | `#e4573d` |
| `brass` | `--brass` | `#e8b54d` |
| `tripod` | `--tripod` | `#5b4a42` |

## Source

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

```tsx
// <EmptyTelescope>: a telescope on a hilltop slowly scanning a round patch of night sky. Stars
// twinkle and now and then a shooting star streaks past. For "no notifications" and "nothing
// new". Animated.

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

export const emptyTelescopeTheme = {
  night: '#2a3566',
  stars: '#fff6d8',
  moon: '#f4e7c4',
  hill: '#8cc66a',
  hillShade: '#74b35a',
  tube: '#e4573d',
  brass: '#e8b54d',
  tripod: '#5b4a42',
};
export type EmptyTelescopeTheme = typeof emptyTelescopeTheme;

// Stars inside the sky disc (centre 120,84, radius 72): [x, y, r, twinkle delay].
const STARS: [number, number, number, number][] = [
  [78, 52, 1.6, 0],
  [102, 34, 1.2, -1.1],
  [150, 40, 1.8, -0.4],
  [172, 70, 1.2, -1.7],
  [92, 82, 1.1, -0.8],
  [64, 92, 1.4, -2.2],
  [128, 62, 1, -1.4],
  [184, 102, 1.4, -0.2],
  [118, 24, 1.3, -1.9],
  [58, 64, 1, -0.6],
];

/** A telescope scanning a quiet night sky: for "no notifications". */
export function EmptyTelescope(props: EmptyStateProps<EmptyTelescopeTheme>) {
  return (
    <EmptyStateFrame
      {...props}
      defaultTheme={emptyTelescopeTheme}
      art={
        <>
          {/* A round patch of night sky */}
          <circle cx="120" cy="84" r="72" fill="var(--night)" />
          <g fill="var(--stars)">
            {STARS.map(([x, y, r, d]) => (
              <circle key={`${x}-${y}`} className={styles.star} cx={x} cy={y} r={r} style={{ animationDelay: `${d}s` }} />
            ))}
          </g>
          <g transform="translate(160 44)">
            <circle r="11" fill="var(--moon)" />
            <circle cx="-3" cy="-2" r="2.4" fill="#000" opacity="0.08" />
            <circle cx="3.5" cy="3" r="1.8" fill="#000" opacity="0.08" />
          </g>

          {/* Shooting star: a short streak across the upper left, every 6s */}
          <g className={styles.shoot}>
            <path d="M0 0 L-22 -9" stroke="var(--stars)" strokeWidth="1.6" strokeLinecap="round" opacity="0.5" />
            <path d="M0 0 L-8 -3.3" stroke="var(--stars)" strokeWidth="2" strokeLinecap="round" />
          </g>

          {/* Hilltop, overlapping the bottom of the sky */}
          <path d="M20 160 C50 128 90 118 124 120 C164 122 200 136 222 160 Z" fill="var(--hill)" />
          <path d="M124 120 C164 122 200 136 222 160 H170 C176 146 160 130 124 120 Z" fill="var(--hill-shade)" />
          <ellipse cx="120" cy="164" rx="104" ry="5" fill="var(--hill-shade)" opacity="0.35" />

          {/* Tripod */}
          <g stroke="var(--tripod)" strokeWidth="3" strokeLinecap="round">
            <path d="M118 104 L100 136 M118 104 L136 136 M118 104 L120 140" />
          </g>

          {/* Telescope tube on its mount, slowly scanning the sky */}
          <g transform="translate(118 102)">
            <g className={styles.scan}>
              <g transform="rotate(-32)">
                <rect x="-22" y="-6" width="52" height="12" rx="3" fill="var(--tube)" />
                <rect x="28" y="-8" width="12" height="16" rx="3" fill="var(--tube)" />
                <rect x="-30" y="-4" width="9" height="8" rx="2" fill="var(--tripod)" />
                <rect x="6" y="-6" width="4" height="12" fill="var(--brass)" />
                <rect x="36" y="-8" width="4" height="16" rx="1" fill="var(--brass)" />
                <path d="M-14 -3 H20" stroke="#ffffff" strokeOpacity="0.35" strokeWidth="2" strokeLinecap="round" />
              </g>
              <circle r="4" fill="var(--brass)" />
            </g>
          </g>
        </>
      }
    />
  );
}
```

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

```css
/* The telescope sweeps slowly back and forth across the sky; stars twinkle on their own beats; a
   shooting star crosses the upper left once every 6s and is hidden the rest of the time. */
.scan { animation: scan 8s ease-in-out infinite alternate; }

.star {
  transform-box: fill-box;
  transform-origin: center;
  animation: twinkle 2.6s ease-in-out infinite;
}

.shoot {
  opacity: 0;
  animation: shoot 6s ease-in infinite;
  animation-delay: -4s;
}

@keyframes scan {
  from { transform: rotate(-10deg); }
  to   { transform: rotate(12deg); }
}
@keyframes twinkle {
  0%, 100% { transform: scale(0.6); opacity: 0.45; }
  50%      { transform: scale(1); opacity: 1; }
}
@keyframes shoot {
  0%, 70%  { transform: translate(92px, 40px); opacity: 0; }
  74%      { opacity: 1; }
  86%      { opacity: 1; }
  92%, 100% { transform: translate(132px, 56px); opacity: 0; }
}
```
