# Aurora Veil

> A slow, flowing aurora drawn by a fragment shader, for screen and hero backgrounds.

- Category: Background
- Version: 1.0.0
- Platforms: iOS, Android, web
- Dependencies: `@shopify/react-native-skia`, `react-native-reanimated`, `react-native-worklets`
- Requires: React Native New Architecture (Reanimated 4)

## Install

```bash
npx kinetik-ui add aurora-veil
```

With the shadcn CLI:

```bash
npx shadcn@latest add https://kinetik-ui.dev/r/aurora-veil.json
```

## Usage

```tsx
import { AuroraVeil } from '@/components/kinetik/aurora-veil';

<AuroraVeil style={StyleSheet.absoluteFill}>
  <Hero />
</AuroraVeil>;
```

The aurora fills its container and draws behind its children. It fades into `baseColor` (the theme background by default) towards the bottom, so text placed low on the surface stays readable.

## When to use

- Hero areas, onboarding and empty states that should feel alive without asking for attention.
- One large surface per screen.

## When not to use

- Behind dense text or forms. Motion behind reading slows people down; use a still frame (`reduceEffects`) or no background.
- Several instances at once. Each one is a full-surface shader.

## Notes

- Pass `active={false}` when the screen loses focus to stop all per-frame work.
- `frameRate={30}` halves the GPU cost on weaker devices; the motion is slow enough that it barely shows.
- Colours come from the theme by default; pass `colors` for brand ribbons.

## AuroraVeilProps

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children`? | `ReactNode` |  | Content drawn on top of the aurora. |
| `colors`? | `[string, string, string]` |  | Ribbon colours, back to front. Defaults to ice, textSubtle and ember from the theme. |
| `baseColor`? | `string` |  | Ground colour behind the ribbons. Defaults to the theme background. |
| `intensity`? | `number` | `0.85` | Strength of the ribbons, 0–1. Defaults to 0.85. |
| `speed`? | `number` | `1` | Animation speed multiplier. Defaults to 1. |
| `frameRate`? | `60 \| 30` | `60` | Shader updates per second. 30 halves the GPU cost on weaker devices. Defaults to 60. |
| `reduceEffects`? | `boolean` | `false` | Draw a single still frame: no per-frame work. Reduced-motion users always get this. |
| `active`? | `boolean` | `true` | Pause the animation, e.g. while the screen is off-screen. Defaults to true. |
| `style`? | `StyleProp<ViewStyle>` |  |  |

## Accessibility

- Reduced motion: A single still frame of the aurora; no per-frame work.
- Screen readers: Decorative: the canvas is hidden from assistive tech. Children stay accessible.
- Touch target: Not interactive.

## Performance

One full-surface fragment shader (4-octave fbm, twice) per frame. frameRate={30} halves the work; reduceEffects draws a single frame.

- Measured: Pixel 9 emulator, API 35, release build · interaction median 1.06× the system Settings app on the same emulator (indicative only) · keeps drawing at rest · 151 MB app memory. Animates by design while active; pass active={false} off-screen. Clock drops to 30 fps with reduceEffects.
- Real-device measurement pending.

## Source

`components/kinetik/aurora-veil.tsx`

```tsx
import { Canvas, Fill, Shader, type SkSize } from '@shopify/react-native-skia';
import type { ReactNode } from 'react';
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';

import { useReduceMotion } from '../../lib/kinetik/hooks/use-reduce-motion';
import { useShaderClock } from '../../lib/kinetik/hooks/use-shader-clock';
import { colorUniform, runtimeEffect } from '../../lib/kinetik/shaders/sksl';
import { useKinetikTheme } from '../../lib/kinetik/tokens/theme';
import { AURORA_SHADER } from './aurora-veil.shader';

export type AuroraVeilProps = {
  /** Content drawn on top of the aurora. */
  children?: ReactNode;
  /** Ribbon colours, back to front. Defaults to ice, textSubtle and ember from the theme. */
  colors?: [string, string, string];
  /** Ground colour behind the ribbons. Defaults to the theme background. */
  baseColor?: string;
  /** Strength of the ribbons, 0–1. Defaults to 0.85. */
  intensity?: number;
  /** Animation speed multiplier. Defaults to 1. */
  speed?: number;
  /** Shader updates per second. 30 halves the GPU cost on weaker devices. Defaults to 60. */
  frameRate?: 60 | 30;
  /** Draw a single still frame: no per-frame work. Reduced-motion users always get this. */
  reduceEffects?: boolean;
  /** Pause the animation, e.g. while the screen is off-screen. Defaults to true. */
  active?: boolean;
  style?: StyleProp<ViewStyle>;
};

/**
 * A slow, flowing aurora drawn by a fragment shader. Use it as a screen or
 * hero background; children render on top.
 */
export function AuroraVeil({
  children,
  colors,
  baseColor,
  intensity = 0.85,
  speed = 1,
  frameRate = 60,
  reduceEffects = false,
  active = true,
  style,
}: AuroraVeilProps) {
  const theme = useKinetikTheme();
  const reduced = useReduceMotion();
  const still = reduced || reduceEffects;
  const size = useSharedValue<SkSize>({ width: 0, height: 0 });
  // Still frames start a few seconds in, where the ribbons are well formed.
  const time = useShaderClock({ running: active && !still, speed, frameRate, start: 4 });

  const [a, b, c] = colors ?? [theme.colors.ice, theme.colors.textSubtle, theme.colors.ember];
  const base = colorUniform(baseColor ?? theme.colors.background);
  const c0 = colorUniform(a);
  const c1 = colorUniform(b);
  const c2 = colorUniform(c);
  const k = Math.max(0, Math.min(1, intensity));

  const uniforms = useDerivedValue(() => ({
    res: [size.get().width, size.get().height],
    time: time.get(),
    intensity: k,
    base,
    c0,
    c1,
    c2,
  }));

  return (
    <View style={[styles.root, style]}>
      {/* Hidden from touch and screen readers on the wrapper: on the web the canvas ignores these props. */}
      <View
        pointerEvents="none"
        style={StyleSheet.absoluteFill}
        accessibilityElementsHidden
        importantForAccessibility="no-hide-descendants"
      >
        <Canvas style={StyleSheet.absoluteFill} onSize={size}>
          <Fill>
            <Shader source={runtimeEffect(AURORA_SHADER)} uniforms={uniforms} />
          </Fill>
        </Canvas>
      </View>
      {children}
    </View>
  );
}

const styles = StyleSheet.create({
  root: { overflow: 'hidden' },
});
```

`components/kinetik/aurora-veil.shader.ts`

```tsx
import { NOISE } from '../../lib/kinetik/shaders/sksl';

/**
 * Three aurora ribbons that sway and shimmer over a base colour.
 * Uniforms: res (px), time (s), intensity (0–1), base + three ribbon colours.
 */
export const AURORA_SHADER = `
uniform float2 res;
uniform float time;
uniform float intensity;
uniform half4 base;
uniform half4 c0;
uniform half4 c1;
uniform half4 c2;
${NOISE}

float ribbon(float y, float center, float width) {
  float d = (y - center) / width;
  return exp(-d * d);
}

half4 main(float2 xy) {
  float2 uv = xy / res;
  float aspect = res.x / max(res.y, 1.0);
  float x = uv.x * aspect;
  float t = time * 0.12;

  // Slow domain warp gives the curtains their drift.
  float warp = fbm(float2(x * 1.3 + t, uv.y * 0.8 - t * 0.6));
  float sway = warp * 0.35;

  // Fine vertical rays, like light through the curtain.
  float rays = 0.55 + 0.45 * fbm(float2(x * 9.0 + warp * 2.0, t * 1.6));

  float r0 = ribbon(uv.y, 0.30 + 0.10 * sin(x * 2.2 + t * 2.6) + sway * 0.2, 0.10) * rays;
  float r1 = ribbon(uv.y, 0.46 + 0.08 * sin(x * 1.6 - t * 2.1 + 1.7) + sway * 0.25, 0.13) * rays;
  float r2 = ribbon(uv.y, 0.22 + 0.12 * sin(x * 2.9 + t * 1.4 + 3.1) - sway * 0.15, 0.07) * rays;

  half3 col = base.rgb;
  col = mix(col, c0.rgb, clamp(r0 * intensity, 0.0, 1.0));
  col = mix(col, c1.rgb, clamp(r1 * intensity * 0.85, 0.0, 1.0));
  col = mix(col, c2.rgb, clamp(r2 * intensity * 0.75, 0.0, 1.0));

  // Fade back into the base towards the bottom so content stays legible.
  col = mix(col, base.rgb, smoothstep(0.45, 1.0, uv.y) * 0.85);
  return half4(col, 1.0);
}
`;
```
