# MoonLoader

> The moon running through its phases from new to full and back every 2.4s, rocking gently among four twinkling stars. The lit part is masked (the disc minus a sliding shadow disc), so it works on any background; a faint dashed outline keeps the new moon visible.

- Page: https://www.boatui.dev/loaders/moon
- Source: https://github.com/wi11s/boatui/blob/main/components/loaders/moon-loader.tsx
- Animated. Generate: ~861 tokens (source size, single pass). Import: ~16 tokens.

## Install

All loaders: `npx degit wi11s/boatui/components/loaders components/loaders`

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

## Usage

```tsx
import { MoonLoader } from '@/components/loaders/moon-loader';

export function Saving() {
  return <MoonLoader size={48} label="Saving your changes…" />;
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `Partial<MoonLoaderTheme>` | — | Colour overrides. Keys listed under theme tokens. |
| `size` | `number` | 64 | Width and height in pixels. |
| `label` | `string` | "Loading…" | Announced to screen readers through role="status". |
| `paused` | `boolean` | false | Freezes the animation. |
| `className` | `string` | — | Applied to the root element (an inline-flex span). |
| `style` | `CSSProperties` | — | Merged into the root element style, after theme variables. |

## Theme tokens

| Key | CSS variable | Default |
| --- | --- | --- |
| `moon` | `--moon` | `#f2d98c` |
| `crater` | `--crater` | `#e2c574` |
| `outline` | `--outline` | `#c9d3dc` |
| `stars` | `--stars` | `#f2c14e` |

## Source

`components/loaders/moon-loader.tsx`

```tsx
// <MoonLoader>: the moon running through its phases among twinkling stars. The lit part is the disc
// minus a shadow disc that slides across it, so it works on any background; a faint outline keeps
// the new moon visible.

import { useId } from 'react';
import { LoaderFrame, type LoaderProps } from './loader';
import styles from './moon-loader.module.css';

export const moonLoaderTheme = {
  moon: '#f2d98c',
  crater: '#e2c574',
  outline: '#c9d3dc',
  stars: '#f2c14e',
};
export type MoonLoaderTheme = typeof moonLoaderTheme;

const R = 26;

/** The moon running through its phases among twinkling stars. */
export function MoonLoader(props: LoaderProps<MoonLoaderTheme>) {
  const id = `qs${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;
  return (
    <LoaderFrame
      {...props}
      defaultTheme={moonLoaderTheme}
      art={
        <>
          <defs>
            {/* White shows the lit part: the disc, minus a black disc that slides across it */}
            <mask id={`${id}phase`} maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
              <circle cx="50" cy="50" r={R} fill="#ffffff" />
              <g className={styles.shadow}>
                <circle cx="50" cy="50" r={R + 1} fill="#000000" />
              </g>
            </mask>
          </defs>

          <circle cx="50" cy="50" r={R} fill="none" stroke="var(--outline)" strokeWidth="1.5" strokeDasharray="2 3" />
          <g mask={`url(#${id}phase)`} className={styles.tilt}>
            <circle cx="50" cy="50" r={R} fill="var(--moon)" />
            <g fill="var(--crater)">
              <circle cx="42" cy="42" r="5" />
              <circle cx="59" cy="57" r="6.5" />
              <circle cx="56" cy="38" r="3" />
              <circle cx="41" cy="61" r="3.5" />
            </g>
          </g>

          <g fill="var(--stars)">
            {[
              { x: 14, y: 22, s: 1, d: 0 },
              { x: 86, y: 18, s: 0.8, d: -0.6 },
              { x: 88, y: 78, s: 1.1, d: -1.2 },
              { x: 12, y: 80, s: 0.7, d: -1.8 },
            ].map((st, i) => (
              <g key={i} transform={`translate(${st.x} ${st.y}) scale(${st.s})`}>
                <path className={styles.star} style={{ animationDelay: `${st.d}s` }} d="M0 -6 L1.5 -1.5 L6 0 L1.5 1.5 L0 6 L-1.5 1.5 L-6 0 L-1.5 -1.5 Z" />
              </g>
            ))}
          </g>
        </>
      }
    />
  );
}
```

`components/loaders/moon-loader.module.css`

```css
/* One 2.4s cycle: the shadow slides from fully covering the disc (new moon) off to the right
   (full), then reappears off the left and slides back over it. The jump from right to left
   happens while both positions are clear of the disc, so the loop is seamless. */
.shadow { animation: phase 2.4s linear infinite; }

.tilt {
  transform-box: fill-box;
  transform-origin: center;
  animation: tilt 4.8s ease-in-out infinite alternate;
}

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

@keyframes phase {
  0%      { transform: translateX(0); }
  45%     { transform: translateX(60px); }
  50%     { transform: translateX(60px); }
  50.01%  { transform: translateX(-60px); }
  55%     { transform: translateX(-60px); }
  100%    { transform: translateX(0); }
}
@keyframes tilt {
  from { transform: rotate(-8deg); }
  to   { transform: rotate(8deg); }
}
@keyframes twinkle {
  0%, 100% { transform: scale(0.5); opacity: 0.3; }
  50%      { transform: scale(1); opacity: 1; }
}
```
