v0.1 public beta early access on GitHub
Home / Screens / misc-halo-memory
Screen template

Halo Memory Screen

Marcel idiom: one continuous animated flow — a glowing halo orb spring-travels between welcome, press, quote and memory-card states

pro Expo Go ✓ iosandroid gestures: tap haptics: light · selection
$ npx lumioui add misc-halo-memory
import { HaloMemoryScreen } from "@/screens/misc/halo-memory";
Registry dependencies2
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
misc/halo-memory.tsxsource · yours to edit
import * as React from "react";
import { View, useWindowDimensions } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated";
import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";
import { Button, Icon, Text } from "@lumioui/ui";
import { cn, spring, useHaptic, useReducedMotion } from "@lumioui/core";
import { Drift, HeroGradient, Mount, Press, SERIF, StickyBottomBar } from "../shared";

/** The halo — a warm glowing orb, the persistent hero of the flow. Pure SVG
 *  radial gradient so the edge feathers to nothing; size/position come from
 *  the parent so each step can move it. */
function Halo({ size }: { size: number }) {
  const id = React.useId().replace(/[^a-zA-Z0-9]/g, "");
  const r = size / 2;
  return (
    <Svg width={size} height={size} accessible={false}>
      <Defs>
        <RadialGradient id={id} cx="50%" cy="50%" r="50%">
          <Stop offset={0} stopColor="#FFFDF4" stopOpacity={1} />
          <Stop offset={0.35} stopColor="#FDEFC9" stopOpacity={0.95} />
          <Stop offset={0.7} stopColor="#FBD9A8" stopOpacity={0.4} />
          <Stop offset={1} stopColor="#FBD9A8" stopOpacity={0} />
        </RadialGradient>
      </Defs>
      <Circle cx={r} cy={r} r={r} fill={`url(#${id})`} />
    </Svg>
  );
}

/** One remembered thing — the white card that rises in the last state. */
function MemoryCard() {
  return (
    <View className="w-full max-w-[300px] rounded-3xl bg-white p-5 shadow-xl shadow-black/10">
      <View className="flex-row items-center gap-2">
        <View className="h-8 w-8 items-center justify-center rounded-full bg-[#EEF2FF]">
          <Icon name="package" size="md" color="#4F46E5" />
        </View>
        <Text className="text-[15px] font-semibold text-[#0B0B0F]">Gift ideas</Text>
      </View>
      <View className="my-3 border-t border-dashed border-[#0B0B0F]/10" />
      <View className="mb-2 self-start rounded-full bg-[#0B0B0F]/[0.05] px-2 py-0.5">
        <Text className="text-[11px] font-medium text-[#0B0B0F]/60">Dad</Text>
      </View>
      {["Garmin watch", "Weekend in Lisbon", "Italian coffee machine"].map((item) => (
        <View key={item} className="flex-row items-center gap-2 py-1">
          <Text className="text-[13px] text-[#0B0B0F]/40">•</Text>
          <Text className="text-[14px] text-[#0B0B0F]">{item}</Text>
        </View>
      ))}
      <View className="mt-3 flex-row items-center gap-1.5">
        <Icon name="refresh" size="sm" color="#0B0B0F" />
        <Text className="text-[12px] text-[#0B0B0F]/40">3 memories found</Text>
      </View>
    </View>
  );
}

/** Orb choreography — each step parks the halo somewhere new: top-center
 *  small for welcome, large centered for press, mid for quote, small top
 *  for the card reveal. `top` is a fraction of the window height. */
const ORB = [
  { top: 0.16, scale: 0.5 },
  { top: 0.2, scale: 1 },
  { top: 0.14, scale: 0.6 },
  { top: 0.1, scale: 0.45 },
] as const;

/** Halo memory — the Marcel idiom: ONE continuous animated flow, not a set
 *  of screens. A pastel dawn world with a glowing halo orb that persists
 *  through four states — welcome, press-the-halo, the remembered quote,
 *  the memory card — while copy and cards crossfade around it. Tapping the
 *  halo (or the pill) advances; it spring-travels to each state's spot. */
export function HaloMemoryScreen({ onDone, onSkip }: { onDone?: () => void; onSkip?: () => void }) {
  const haptics = useHaptic();
  const reduce = useReducedMotion();
  const [step, setStep] = React.useState(0);
  const { height } = useWindowDimensions();
  const top = useSharedValue<number>(ORB[0].top);
  const scale = useSharedValue<number>(ORB[0].scale);
  React.useEffect(() => {
    const t = ORB[step] ?? ORB[0];
    top.value = reduce ? t.top : withSpring(t.top, { damping: 18, stiffness: 90 });
    scale.value = reduce ? t.scale : withSpring(t.scale, spring.layout);
  }, [step, reduce, top, scale]);

  const orbStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: -110 }, { translateY: top.value * height }, { scale: scale.value }],
  }));

  const advance = () => {
    haptics.light();
    setStep((s) => (s < 3 ? s + 1 : 0));
  };

  const cta = step === 0 ? "Begin" : step === 1 ? "Tap the halo" : step === 3 ? "Continue" : null;

  return (
    <Press accessibilityRole="button" accessibilityLabel="Continue" onPress={advance} className="flex-1 bg-[#BFD3EE]">
      {/* Dawn world — cool blue over warm peach, the Marcel gradient. */}
      <HeroGradient from="#AFC4E8" to="#E8D9CE" />
      <View className="absolute inset-x-0 bottom-0 h-[420px]">
        <HeroGradient from="rgba(253,231,186,0)" to="#F5C99A" />
      </View>
      <Drift emoji="☁️" cls="left-6 top-24" size="text-[38px]" rotate={0} index={0} />
      <Drift emoji="🕊️" cls="right-8 top-[380px]" size="text-[30px]" rotate={8} index={1} />

      {/* The halo — always mounted, spring-travels between states. */}
      <Animated.View
        pointerEvents="none"
        className="absolute left-1/2 items-center justify-center"
        style={[orbStyle, { width: 220, height: 220 }]}
      >
        <Halo size={220} />
      </Animated.View>

      <SafeAreaView className="flex-1" edges={["top", "bottom"]}>
        <View className="flex-row items-center justify-between px-4 pt-1">
          <Press
            onPress={onSkip}
            hitSlop={12}
            accessibilityRole="button"
            accessibilityLabel="Back"
            className="flex-row items-center"
          >
            <Icon name="chevron-left" size="md" color="#3D4C6B" />
            <Text className="text-[14px] font-medium text-[#3D4C6B]">Happily</Text>
          </Press>
          {step > 0 ? (
            <Press
              onPress={() => {
                haptics.selection();
                onSkip?.();
              }}
              hitSlop={12}
              accessibilityRole="button"
              accessibilityLabel="Skip"
            >
              <Text className="text-[14px] font-medium text-[#3D4C6B]/70">Skip</Text>
            </Press>
          ) : (
            <View />
          )}
        </View>

        {/* State copy — each step's text lives in the same column so the
            world reads as continuous while content swaps. */}
        <View className="flex-1 items-center justify-end px-8 pb-4">
          {step === 0 && (
            <Mount className="items-center gap-3 pb-52">
              <Text
                style={{ fontFamily: SERIF }}
                className="text-center text-[30px] leading-tight text-[#2A3A5C]"
              >
                Everything you{"\n"}don't want to forget.
              </Text>
              <Text className="text-center text-[15px] text-[#2A3A5C]/60">
                Lumio remembers it for you.
              </Text>
            </Mount>
          )}

          {step === 1 && (
            <Mount className="items-center gap-3 pb-8">
              <Text className="text-center text-[15px] font-medium text-[#2A3A5C]">
                Try Lumio.
              </Text>
              <Text className="text-center text-[13px] text-[#2A3A5C]/60">
                Tap the halo to begin
              </Text>
            </Mount>
          )}

          {step === 2 && (
            <Mount className="items-center gap-3 pb-20">
              <Text className="text-center text-[13px] text-[#2A3A5C]/60">
                Tap the halo and say:
              </Text>
              <Text
                style={{ fontFamily: SERIF }}
                className="text-center text-[24px] leading-snug text-[#2A3A5C]"
              >
                « Julie recommended{"\n"}the Tuba restaurant{"\n"}in Marseille. »
              </Text>
              <Press
                onPress={advance}
                accessibilityRole="button"
                accessibilityLabel="Next"
                className="mt-4 rounded-full bg-white/70 px-6 py-2.5"
              >
                <Text className="text-[14px] font-semibold text-[#2A3A5C]">Next ›</Text>
              </Press>
            </Mount>
          )}

          {step === 3 && (
            <Mount className="w-full items-center gap-3 pb-36">
              <Text className="text-center text-[13px] text-[#2A3A5C]/60">
                Tap the halo and ask:
              </Text>
              <Text
                style={{ fontFamily: SERIF }}
                className="text-center text-[22px] leading-snug text-[#2A3A5C]"
              >
                « What gift ideas did I{"\n"}note for my dad? »
              </Text>
              <MemoryCard />
            </Mount>
          )}
        </View>

        {cta && (
          <StickyBottomBar className="px-8">
            <Button
              size="lg"
              className={cn("w-full", step === 0 ? "bg-white/80" : "bg-[#2A3A5C]")}
              textClassName={step === 0 ? "text-[#2A3A5C]" : "text-white"}
              onPress={() => {
                if (step === 3) onDone?.();
                advance();
              }}
              accessibilityLabel={cta}
            >
              {cta}
            </Button>
          </StickyBottomBar>
        )}
      </SafeAreaView>
    </Press>
  );
}
HaloMemoryScreen.displayName = "HaloMemoryScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Halo Memory Screen" screen in React Native using LumioUI.

Goal: Marcel idiom: one continuous animated flow — a glowing halo orb spring-travels between welcome, press, quote and memory-card states Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add misc-halo-memory
```

Or pull just the components: `npx lumioui add button text`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×16

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/misc/halo-memory.tsx` exporting HaloMemoryScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { View, useWindowDimensions } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from "react-native-reanimated";
import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";
import { Button, Icon, Text } from "@lumioui/ui";
import { cn, spring, useHaptic, useReducedMotion } from "@lumioui/core";
import { Drift, HeroGradient, Mount, Press, SERIF, StickyBottomBar } from "../shared";

/** The halo — a warm glowing orb, the persistent hero of the flow. Pure SVG
 *  radial gradient so the edge feathers to nothing; size/position come from
 *  the parent so each step can move it. */
function Halo({ size }: { size: number }) {
  const id = React.useId().replace(/[^a-zA-Z0-9]/g, "");
  const r = size / 2;
  return (
    <Svg width={size} height={size} accessible={false}>
      <Defs>
        <RadialGradient id={id} cx="50%" cy="50%" r="50%">
          <Stop offset={0} stopColor="#FFFDF4" stopOpacity={1} />
          <Stop offset={0.35} stopColor="#FDEFC9" stopOpacity={0.95} />
          <Stop offset={0.7} stopColor="#FBD9A8" stopOpacity={0.4} />
          <Stop offset={1} stopColor="#FBD9A8" stopOpacity={0} />
        </RadialGradient>
      </Defs>
      <Circle cx={r} cy={r} r={r} fill={`url(#${id})`} />
    </Svg>
  );
}

/** One remembered thing — the white card that rises in the last state. */
function MemoryCard() {
  return (
    <View className="w-full max-w-[300px] rounded-3xl bg-white p-5 shadow-xl shadow-black/10">
      <View className="flex-row items-center gap-2">
        <View className="h-8 w-8 items-center justify-center rounded-full bg-[#EEF2FF]">
          <Icon name="package" size="md" color="#4F46E5" />
        </View>
        <Text className="text-[15px] font-semibold text-[#0B0B0F]">Gift ideas</Text>
      </View>
      <View className="my-3 border-t border-dashed border-[#0B0B0F]/10" />
      <View className="mb-2 self-start rounded-full bg-[#0B0B0F]/[0.05] px-2 py-0.5">
        <Text className="text-[11px] font-medium text-[#0B0B0F]/60">Dad</Text>
      </View>
      {["Garmin watch", "Weekend in Lisbon", "Italian coffee machine"].map((item) => (
        <View key={item} className="flex-row items-center gap-2 py-1">
          <Text className="text-[13px] text-[#0B0B0F]/40">•</Text>
          <Text className="text-[14px] text-[#0B0B0F]">{item}</Text>
        </View>
      ))}
      <View className="mt-3 flex-row items-center gap-1.5">
        <Icon name="refresh" size="sm" color="#0B0B0F" />
        <Text className="text-[12px] text-[#0B0B0F]/40">3 memories found</Text>
      </View>
    </View>
  );
}

/** Orb choreography — each step parks the halo somewhere new: top-center
 *  small for welcome, large centered for press, mid for quote, small top
 *  for the card reveal. `top` is a fraction of the window height. */
const ORB = [
  { top: 0.16, scale: 0.5 },
  { top: 0.2, scale: 1 },
  { top: 0.14, scale: 0.6 },
  { top: 0.1, scale: 0.45 },
] as const;

/** Halo memory — the Marcel idiom: ONE continuous animated flow, not a set
 *  of screens. A pastel dawn world with a glowing halo orb that persists
 *  through four states — welcome, press-the-halo, the remembered quote,
 *  the memory card — while copy and cards crossfade around it. Tapping the
 *  halo (or the pill) advances; it spring-travels to each state's spot. */
export function HaloMemoryScreen({ onDone, onSkip }: { onDone?: () => void; onSkip?: () => void }) {
  const haptics = useHaptic();
  const reduce = useReducedMotion();
  const [step, setStep] = React.useState(0);
  const { height } = useWindowDimensions();
  const top = useSharedValue<number>(ORB[0].top);
  const scale = useSharedValue<number>(ORB[0].scale);
  React.useEffect(() => {
    const t = ORB[step] ?? ORB[0];
    top.value = reduce ? t.top : withSpring(t.top, { damping: 18, stiffness: 90 });
    scale.value = reduce ? t.scale : withSpring(t.scale, spring.layout);
  }, [step, reduce, top, scale]);

  const orbStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: -110 }, { translateY: top.value * height }, { scale: scale.value }],
  }));

  const advance = () => {
    haptics.light();
    setStep((s) => (s < 3 ? s + 1 : 0));
  };

  const cta = step === 0 ? "Begin" : step === 1 ? "Tap the halo" : step === 3 ? "Continue" : null;

  return (
    <Press accessibilityRole="button" accessibilityLabel="Continue" onPress={advance} className="flex-1 bg-[#BFD3EE]">
      {/* Dawn world — cool blue over warm peach, the Marcel gradient. */}
      <HeroGradient from="#AFC4E8" to="#E8D9CE" />
      <View className="absolute inset-x-0 bottom-0 h-[420px]">
        <HeroGradient from="rgba(253,231,186,0)" to="#F5C99A" />
      </View>
      <Drift emoji="☁️" cls="left-6 top-24" size="text-[38px]" rotate={0} index={0} />
      <Drift emoji="🕊️" cls="right-8 top-[380px]" size="text-[30px]" rotate={8} index={1} />

      {/* The halo — always mounted, spring-travels between states. */}
      <Animated.View
        pointerEvents="none"
        className="absolute left-1/2 items-center justify-center"
        style={[orbStyle, { width: 220, height: 220 }]}
      >
        <Halo size={220} />
      </Animated.View>

      <SafeAreaView className="flex-1" edges={["top", "bottom"]}>
        <View className="flex-row items-center justify-between px-4 pt-1">
          <Press
            onPress={onSkip}
            hitSlop={12}
            accessibilityRole="button"
            accessibilityLabel="Back"
            className="flex-row items-center"
          >
            <Icon name="chevron-left" size="md" color="#3D4C6B" />
            <Text className="text-[14px] font-medium text-[#3D4C6B]">Happily</Text>
          </Press>
          {step > 0 ? (
            <Press
              onPress={() => {
                haptics.selection();
                onSkip?.();
              }}
              hitSlop={12}
              accessibilityRole="button"
              accessibilityLabel="Skip"
            >
              <Text className="text-[14px] font-medium text-[#3D4C6B]/70">Skip</Text>
            </Press>
          ) : (
            <View />
          )}
        </View>

        {/* State copy — each step's text lives in the same column so the
            world reads as continuous while content swaps. */}
        <View className="flex-1 items-center justify-end px-8 pb-4">
          {step === 0 && (
            <Mount className="items-center gap-3 pb-52">
              <Text
                style={{ fontFamily: SERIF }}
                className="text-center text-[30px] leading-tight text-[#2A3A5C]"
              >
                Everything you{"\n"}don't want to forget.
              </Text>
              <Text className="text-center text-[15px] text-[#2A3A5C]/60">
                Lumio remembers it for you.
              </Text>
            </Mount>
          )}

          {step === 1 && (
            <Mount className="items-center gap-3 pb-8">
              <Text className="text-center text-[15px] font-medium text-[#2A3A5C]">
                Try Lumio.
              </Text>
              <Text className="text-center text-[13px] text-[#2A3A5C]/60">
                Tap the halo to begin
              </Text>
            </Mount>
          )}

          {step === 2 && (
            <Mount className="items-center gap-3 pb-20">
              <Text className="text-center text-[13px] text-[#2A3A5C]/60">
                Tap the halo and say:
              </Text>
              <Text
                style={{ fontFamily: SERIF }}
                className="text-center text-[24px] leading-snug text-[#2A3A5C]"
              >
                « Julie recommended{"\n"}the Tuba restaurant{"\n"}in Marseille. »
              </Text>
              <Press
                onPress={advance}
                accessibilityRole="button"
                accessibilityLabel="Next"
                className="mt-4 rounded-full bg-white/70 px-6 py-2.5"
              >
                <Text className="text-[14px] font-semibold text-[#2A3A5C]">Next ›</Text>
              </Press>
            </Mount>
          )}

          {step === 3 && (
            <Mount className="w-full items-center gap-3 pb-36">
              <Text className="text-center text-[13px] text-[#2A3A5C]/60">
                Tap the halo and ask:
              </Text>
              <Text
                style={{ fontFamily: SERIF }}
                className="text-center text-[22px] leading-snug text-[#2A3A5C]"
              >
                « What gift ideas did I{"\n"}note for my dad? »
              </Text>
              <MemoryCard />
            </Mount>
          )}
        </View>

        {cta && (
          <StickyBottomBar className="px-8">
            <Button
              size="lg"
              className={cn("w-full", step === 0 ? "bg-white/80" : "bg-[#2A3A5C]")}
              textClassName={step === 0 ? "text-[#2A3A5C]" : "text-white"}
              onPress={() => {
                if (step === 3) onDone?.();
                advance();
              }}
              accessibilityLabel={cta}
            >
              {cta}
            </Button>
          </StickyBottomBar>
        )}
      </SafeAreaView>
    </Press>
  );
}
HaloMemoryScreen.displayName = "HaloMemoryScreen";
```

Screen: https://lumioui.dev/screens/misc-halo-memory
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗