{"slug":"alert","name":"Alert","description":"Feedback that eases in and folds away.","group":"Motion","detail_url":"https://beui.dev/native/r/alert","page_url":"https://beui.dev/react-native/alert","expo_go_url":"exp://u.expo.dev/93a3a95c-6dc2-4adb-b57c-efbecf859f25/--/alert?runtime-version=exposdk%3A57.0.0&channel-name=production","source_url":"https://github.com/starc007/beui-native/tree/main/src/components/alert","libraries":["react","react-native","react-native-reanimated","react-native-worklets","uniwind"],"install":["npx expo install react-native-reanimated react-native-worklets","npm install uniwind tailwindcss"],"setup":["Quickest: run `npx beui-native add <slug>` from the app's root. It writes the files below and installs the libraries.","Copy the files into the app and keep their folder layout: components import the shared files by relative path.","Add the theme tokens once, imported after Tailwind and Uniwind in the CSS file Uniwind reads.","Wrap the app in GestureHandlerRootView if it uses react-native-gesture-handler and isn't already.","Works in Expo and in bare React Native. Every component honors the system Reduce Motion setting."],"files":[{"path":"src/components/alert/alert.tsx","content":"import {\n  createContext,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type ColorValue,\n  Pressable,\n  type PressableProps,\n  StyleSheet,\n  Text,\n  type TextProps,\n  View,\n  type ViewProps,\n} from \"react-native\";\nimport Animated, {\n  interpolateColor,\n  useAnimatedStyle,\n  useSharedValue,\n  withSpring,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { scheduleOnRN } from \"react-native-worklets\";\nimport { useResolveClassNames } from \"uniwind\";\nimport { useReducedMotionPreference } from \"../../hooks/use-reduced-motion\";\nimport { DrawnPath, type PathSegment } from \"../../primitives/drawn-path\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_PRESS } from \"../../styles/motion\";\n\nexport type AlertVariant =\n  | \"default\"\n  | \"info\"\n  | \"success\"\n  | \"warning\"\n  | \"destructive\";\n\nexport type AlertState = {\n  open: boolean;\n  variant: AlertVariant;\n  setOpen: (open: boolean) => void;\n  dismiss: () => void;\n};\n\ntype AlertContextValue = AlertState & { reduce: boolean };\n\nconst AlertContext = createContext<AlertContextValue | null>(null);\n\nfunction useAlertContext(component: string) {\n  const context = useContext(AlertContext);\n  if (!context) throw new Error(`${component} must be used within <Alert>`);\n  return context;\n}\n\n/** Shared visibility and variant for custom alert children. */\nexport function useAlert(): AlertState {\n  return useAlertContext(\"useAlert\");\n}\n\n// Full class strings, so Uniwind can see every token at build time.\nconst SURFACE_CLASS: Record<AlertVariant, string> = {\n  default: \"bg-surface\",\n  info: \"bg-primary/5\",\n  success: \"bg-success/5\",\n  warning: \"bg-warning/5\",\n  destructive: \"bg-destructive/5\",\n};\nconst TONE_CLASS: Record<AlertVariant, string> = {\n  default: \"text-foreground\",\n  info: \"text-primary\",\n  success: \"text-success\",\n  warning: \"text-warning\",\n  destructive: \"text-destructive\",\n};\n\nconst ENTER_FADE = { duration: 180, easing: EASE_OUT };\nconst ENTER_RISE = { duration: 200, easing: EASE_OUT };\nconst EXIT = { duration: 120, easing: EASE_OUT };\nconst COLLAPSE = { duration: 220, easing: EASE_OUT };\nconst COLOR_FADE = { duration: 200, easing: EASE_OUT };\nconst ICON_IN = { duration: 180, easing: EASE_OUT };\nconst ICON_OUT = { duration: 100, easing: EASE_OUT };\nconst DROP = 6;\nconst ICON = 16;\n\nexport type AlertProps = Omit<ViewProps, \"children\"> & {\n  /** Defaults to \"default\". */\n  variant?: AlertVariant;\n  /** Controlled visibility. Omit it and use `defaultOpen` for uncontrolled use. */\n  open?: boolean;\n  /** Defaults to true. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Layout classes for the inner row, separate from the animated surface. */\n  contentClassName?: string;\n  /** Opens and closes without moving, independently of the system preference. */\n  disableAnimation?: boolean;\n  className?: string;\n  children?: ReactNode;\n  ref?: Ref<View>;\n};\n\n/**\n * Feedback that eases in, folds away when dismissed, and blends between\n * variants. Compose AlertIcon, AlertContent (AlertTitle, AlertDescription,\n * AlertAction) and AlertClose inside.\n */\nexport function Alert({\n  variant = \"default\",\n  open: controlledOpen,\n  defaultOpen = true,\n  onOpenChange,\n  contentClassName = \"\",\n  disableAnimation = false,\n  className = \"\",\n  style,\n  children,\n  accessibilityRole,\n  accessibilityLiveRegion,\n  ref,\n  ...props\n}: AlertProps) {\n  const reducedMotion = useReducedMotionPreference();\n  const reduce = reducedMotion || disableAnimation;\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const open = controlledOpen ?? internalOpen;\n  const state = useRef({ open, controlled: false, onOpenChange });\n  state.current = {\n    open,\n    controlled: controlledOpen !== undefined,\n    onOpenChange,\n  };\n\n  const setOpen = useCallback((next: boolean) => {\n    const current = state.current;\n    if (next === current.open) return;\n    if (!current.controlled) setInternalOpen(next);\n    current.onOpenChange?.(next);\n  }, []);\n  const dismiss = useCallback(() => setOpen(false), [setOpen]);\n  const context = useMemo(\n    () => ({ open, variant, reduce, setOpen, dismiss }),\n    [dismiss, open, reduce, setOpen, variant],\n  );\n\n  // Stays mounted through the exit, then unmounts.\n  const [mounted, setMounted] = useState(open);\n  if (open && !mounted) setMounted(true);\n  // Set while opening, until a height is known to grow into. An alert open on\n  // the first render lands at rest, like initial={false}.\n  const appearing = useRef(false);\n  const [wasOpen, setWasOpen] = useState(open);\n  if (open !== wasOpen) {\n    setWasOpen(open);\n    appearing.current = open;\n  }\n  const lastHeight = useRef(0);\n\n  const height = useSharedValue(-1);\n  const opacity = useSharedValue(open ? 1 : 0);\n  const rise = useSharedValue(0);\n\n  const enter = useCallback(\n    (to: number) => {\n      appearing.current = false;\n      opacity.value = withTiming(1, ENTER_FADE);\n      if (reduce) {\n        height.value = to;\n        rise.value = 0;\n        return;\n      }\n      height.value = withSpring(to, SPRING_LAYOUT);\n      rise.value = withTiming(0, ENTER_RISE);\n    },\n    [height, opacity, reduce, rise],\n  );\n\n  useEffect(() => {\n    if (open) {\n      // Reopened before the exit finished: no new layout will arrive, so grow\n      // back to the size already known.\n      if (appearing.current && lastHeight.current > 0 && height.value > 0) {\n        enter(lastHeight.current);\n      } else if (appearing.current) {\n        height.value = 0;\n        opacity.value = 0;\n        rise.value = reduce ? 0 : -DROP;\n      }\n      return;\n    }\n    const unmount = () => setMounted(false);\n    if (reduce) {\n      height.value = 0;\n      opacity.value = withTiming(0, EXIT, (finished) => {\n        if (finished) scheduleOnRN(unmount);\n      });\n      return;\n    }\n    opacity.value = withTiming(0, EXIT);\n    rise.value = withTiming(-DROP, EXIT);\n    // The space folds away so whatever sits below glides up into it.\n    height.value = withTiming(0, COLLAPSE, (finished) => {\n      if (finished) scheduleOnRN(unmount);\n    });\n  }, [enter, height, open, opacity, reduce, rise]);\n\n  const measure = (next: number) => {\n    lastHeight.current = next;\n    if (!open) return;\n    if (appearing.current) {\n      enter(next);\n      return;\n    }\n    // Content changes, such as a new variant's text, spring to the new size.\n    height.value =\n      height.value < 0 || reduce ? next : withSpring(next, SPRING_LAYOUT);\n  };\n\n  // The surface color blends between variants.\n  const { backgroundColor } = useResolveClassNames(SURFACE_CLASS[variant]);\n  const surface = String(backgroundColor ?? \"transparent\");\n  const shown = useRef(surface);\n  const blend = useSharedValue(1);\n  const palette = useSharedValue({ from: surface, to: surface });\n  useEffect(() => {\n    if (shown.current === surface) return;\n    palette.value = { from: shown.current, to: surface };\n    shown.current = surface;\n    blend.value = 0;\n    blend.value = reduce ? 1 : withTiming(1, COLOR_FADE);\n  }, [blend, palette, reduce, surface]);\n\n  const frameStyle = useAnimatedStyle(() =>\n    height.value < 0 ? {} : { height: Math.max(0, height.value) },\n  );\n  const surfaceStyle = useAnimatedStyle(() => ({\n    opacity: opacity.value,\n    transform: [{ translateY: rise.value }],\n    backgroundColor: interpolateColor(\n      blend.value,\n      [0, 1],\n      [palette.value.from, palette.value.to],\n    ),\n  }));\n\n  if (!mounted) return null;\n\n  const urgent = variant === \"warning\" || variant === \"destructive\";\n\n  return (\n    <AlertContext.Provider value={context}>\n      <Animated.View style={[styles.frame, frameStyle]}>\n        <Animated.View\n          {...props}\n          ref={ref}\n          accessibilityRole={\n            accessibilityRole ?? (urgent ? \"alert\" : undefined)\n          }\n          accessibilityLiveRegion={\n            accessibilityLiveRegion ?? (urgent ? \"assertive\" : \"polite\")\n          }\n          importantForAccessibility={open ? \"auto\" : \"no-hide-descendants\"}\n          accessibilityElementsHidden={!open}\n          onLayout={(event) => {\n            measure(event.nativeEvent.layout.height);\n            props.onLayout?.(event);\n          }}\n          className={`rounded-xl p-4 ${className}`}\n          style={[styles.surface, surfaceStyle, style]}\n        >\n          <View className={`flex-row items-start gap-3 ${contentClassName}`}>\n            {children}\n          </View>\n        </Animated.View>\n      </Animated.View>\n    </AlertContext.Provider>\n  );\n}\n\nconst CIRCLE_GLYPHS: Partial<Record<AlertVariant, PathSegment[]>> = {\n  default: [\n    [12, 16, 12, 12],\n    [12, 8, 12, 8.01],\n  ],\n  info: [\n    [12, 16, 12, 12],\n    [12, 8, 12, 8.01],\n  ],\n  success: [\n    [8.5, 12, 11, 14.5],\n    [11, 14.5, 15.5, 9.5],\n  ],\n  destructive: [\n    [12, 8, 12, 12],\n    [12, 16, 12, 16.01],\n  ],\n};\nconst WARNING_GLYPH: PathSegment[] = [\n  [12, 3.5, 21.5, 20],\n  [21.5, 20, 2.5, 20],\n  [2.5, 20, 12, 3.5],\n  [12, 9.5, 12, 13.5],\n  [12, 16.8, 12, 16.81],\n];\n\nfunction VariantIcon({\n  variant,\n  color,\n}: {\n  variant: AlertVariant;\n  color: ColorValue | undefined;\n}) {\n  const drawn = useSharedValue(1);\n  const unit = ICON / 24;\n  if (variant === \"warning\") {\n    return (\n      <DrawnPath\n        segments={WARNING_GLYPH}\n        size={ICON}\n        strokeWidth={2}\n        color={color}\n        progress={drawn}\n      />\n    );\n  }\n  return (\n    <View style={styles.icon}>\n      <View\n        style={{\n          position: \"absolute\",\n          width: 20 * unit,\n          height: 20 * unit,\n          borderRadius: 10 * unit,\n          borderWidth: 2 * unit,\n          borderColor: color,\n        }}\n      />\n      <DrawnPath\n        segments={CIRCLE_GLYPHS[variant] ?? []}\n        size={ICON}\n        strokeWidth={2}\n        color={color}\n        progress={drawn}\n      />\n    </View>\n  );\n}\n\ntype IconLayer = { variant: AlertVariant; leaving: boolean; id: number };\n\nfunction IconLayerView({\n  layer,\n  reduce,\n  appear,\n  color,\n  children,\n  onExited,\n}: {\n  layer: IconLayer;\n  reduce: boolean;\n  appear: boolean;\n  color: ColorValue | undefined;\n  children?: ReactNode;\n  onExited: (id: number) => void;\n}) {\n  const enter = appear && !reduce;\n  const opacity = useSharedValue(enter ? 0 : 1);\n  const lift = useSharedValue(enter ? 4 : 0);\n  const scale = useSharedValue(enter ? 0.9 : 1);\n  const started = useRef(false);\n\n  useEffect(() => {\n    if (layer.leaving) {\n      const done = () => onExited(layer.id);\n      if (reduce) {\n        done();\n        return;\n      }\n      lift.value = withTiming(-4, ICON_OUT);\n      scale.value = withTiming(0.9, ICON_OUT);\n      opacity.value = withTiming(0, ICON_OUT, (finished) => {\n        if (finished) scheduleOnRN(done);\n      });\n      return;\n    }\n    if (started.current || !enter) return;\n    started.current = true;\n    opacity.value = withTiming(1, ICON_IN);\n    lift.value = withTiming(0, ICON_IN);\n    scale.value = withTiming(1, ICON_IN);\n  }, [enter, layer, lift, onExited, opacity, reduce, scale]);\n\n  const style = useAnimatedStyle(() => ({\n    opacity: opacity.value,\n    transform: [{ translateY: lift.value }, { scale: scale.value }],\n  }));\n\n  return (\n    <Animated.View style={[StyleSheet.absoluteFill, styles.icon, style]}>\n      {children ?? <VariantIcon variant={layer.variant} color={color} />}\n    </Animated.View>\n  );\n}\n\nexport type AlertIconProps = Omit<ViewProps, \"children\"> & {\n  /** Replaces the variant icon. Given no color, so pass one that fits. */\n  children?: ReactNode;\n  className?: string;\n};\n\n/** The variant's icon. It swaps with a small rise when the variant changes. */\nexport function AlertIcon({\n  children,\n  className = \"\",\n  style,\n  ...props\n}: AlertIconProps) {\n  const { variant, reduce } = useAlertContext(\"AlertIcon\");\n  const { color } = useResolveClassNames(TONE_CLASS[variant]);\n  const nextId = useRef(1);\n  const [layers, setLayers] = useState<IconLayer[]>([\n    { variant, leaving: false, id: 0 },\n  ]);\n  const [shown, setShown] = useState(variant);\n  const mounted = useRef(false);\n  useEffect(() => {\n    mounted.current = true;\n  }, []);\n\n  if (variant !== shown) {\n    setShown(variant);\n    const id = nextId.current++;\n    setLayers((current) => [\n      ...current.map((layer) => ({ ...layer, leaving: true })),\n      { variant, leaving: false, id },\n    ]);\n  }\n\n  const exited = useCallback((id: number) => {\n    setLayers((current) => current.filter((layer) => layer.id !== id));\n  }, []);\n\n  return (\n    <View\n      {...props}\n      accessibilityElementsHidden\n      importantForAccessibility=\"no-hide-descendants\"\n      className={`mt-0.5 ${className}`}\n      style={[styles.iconSlot, style]}\n    >\n      {layers.map((layer) => (\n        <IconLayerView\n          key={layer.id}\n          layer={layer}\n          reduce={reduce}\n          appear={mounted.current && layer.id !== 0}\n          color={color}\n          onExited={exited}\n        >\n          {children}\n        </IconLayerView>\n      ))}\n    </View>\n  );\n}\n\n/** Stacks the title, description and actions, and takes the free width. */\nexport function AlertContent({\n  className = \"\",\n  ...props\n}: ViewProps & { className?: string }) {\n  return <View {...props} className={`min-w-0 flex-1 gap-1 ${className}`} />;\n}\n\n/** Takes the variant's color. */\nexport function AlertTitle({\n  className = \"\",\n  ...props\n}: TextProps & { className?: string }) {\n  const { variant } = useAlertContext(\"AlertTitle\");\n  return (\n    <Text\n      {...props}\n      className={`text-sm leading-5 font-medium ${TONE_CLASS[variant]} ${className}`}\n    />\n  );\n}\n\nexport function AlertDescription({\n  className = \"\",\n  ...props\n}: TextProps & { className?: string }) {\n  return (\n    <Text\n      {...props}\n      className={`text-sm leading-relaxed text-muted ${className}`}\n    />\n  );\n}\n\n/** A row for buttons. Compose them here. */\nexport function AlertAction({\n  className = \"\",\n  ...props\n}: ViewProps & { className?: string }) {\n  return (\n    <View\n      {...props}\n      className={`flex-row flex-wrap items-center gap-2 pt-2 ${className}`}\n    />\n  );\n}\n\nconst CLOSE_GLYPH: PathSegment[] = [\n  [18, 6, 6, 18],\n  [6, 6, 18, 18],\n];\n\nexport type AlertCloseProps = Omit<PressableProps, \"children\" | \"style\"> & {\n  /** Replaces the X. */\n  children?: ReactNode;\n  className?: string;\n};\n\n/** Dismisses the alert. Your onPress runs first. */\nexport function AlertClose({\n  children,\n  className = \"\",\n  onPress,\n  onPressIn,\n  onPressOut,\n  accessibilityLabel = \"Dismiss alert\",\n  ...props\n}: AlertCloseProps) {\n  const { dismiss, reduce } = useAlertContext(\"AlertClose\");\n  const { color } = useResolveClassNames(\"text-muted\");\n  const { backgroundColor: pressedColor } =\n    useResolveClassNames(\"bg-foreground/5\");\n  const scale = useSharedValue(1);\n  const pressed = useSharedValue(0);\n  const drawn = useSharedValue(1);\n\n  const style = useAnimatedStyle(() => ({\n    transform: [{ scale: scale.value }],\n    backgroundColor: interpolateColor(\n      pressed.value,\n      [0, 1],\n      [\"transparent\", String(pressedColor ?? \"transparent\")],\n    ),\n  }));\n\n  return (\n    <Pressable\n      {...props}\n      accessibilityRole=\"button\"\n      accessibilityLabel={accessibilityLabel}\n      hitSlop={6}\n      onPress={(event) => {\n        onPress?.(event);\n        dismiss();\n      }}\n      onPressIn={(event) => {\n        pressed.value = withTiming(1, { duration: 80 });\n        if (!reduce) scale.value = withSpring(0.94, SPRING_PRESS);\n        onPressIn?.(event);\n      }}\n      onPressOut={(event) => {\n        pressed.value = withTiming(0, { duration: 200 });\n        if (!reduce) scale.value = withSpring(1, SPRING_PRESS);\n        onPressOut?.(event);\n      }}\n      className={`-m-1.5 ${className}`}\n    >\n      <Animated.View style={[styles.close, style]}>\n        {children ?? (\n          <DrawnPath\n            segments={CLOSE_GLYPH}\n            size={ICON}\n            strokeWidth={2}\n            color={color}\n            progress={drawn}\n          />\n        )}\n      </Animated.View>\n    </Pressable>\n  );\n}\n\nconst styles = StyleSheet.create({\n  frame: { width: \"100%\", overflow: \"hidden\" },\n  // Pinned to the top so the frame can fold around it from below.\n  surface: { position: \"absolute\", top: 0, left: 0, right: 0 },\n  iconSlot: { width: ICON, height: ICON },\n  icon: { alignItems: \"center\", justifyContent: \"center\" },\n  close: {\n    width: 32,\n    height: 32,\n    borderRadius: 8,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n});\n","type":"component"},{"path":"src/components/alert/index.ts","content":"export type {\n  AlertCloseProps,\n  AlertIconProps,\n  AlertProps,\n  AlertState,\n  AlertVariant,\n} from \"./alert\";\nexport {\n  Alert,\n  AlertAction,\n  AlertClose,\n  AlertContent,\n  AlertDescription,\n  AlertIcon,\n  AlertTitle,\n  useAlert,\n} from \"./alert\";\n","type":"component"},{"path":"src/hooks/use-reduced-motion.ts","content":"import { useEffect, useState } from \"react\";\nimport { AccessibilityInfo } from \"react-native\";\nimport { useReducedMotion } from \"react-native-reanimated\";\n\n/** Reanimated supplies a synchronous initial value; native events keep it current. */\nexport function useReducedMotionPreference() {\n  const initialValue = useReducedMotion();\n  const [reducedMotion, setReducedMotion] = useState(initialValue);\n\n  useEffect(() => {\n    let active = true;\n    let receivedEvent = false;\n    const subscription = AccessibilityInfo.addEventListener(\n      \"reduceMotionChanged\",\n      (value) => {\n        receivedEvent = true;\n        setReducedMotion(value);\n      },\n    );\n    void AccessibilityInfo.isReduceMotionEnabled().then((value) => {\n      if (active && !receivedEvent) setReducedMotion(value);\n    });\n    return () => {\n      active = false;\n      subscription.remove();\n    };\n  }, []);\n\n  return reducedMotion;\n}\n","type":"shared"},{"path":"src/primitives/drawn-path.tsx","content":"import { type ColorValue, StyleSheet, View } from \"react-native\";\nimport Animated, {\n  interpolate,\n  type SharedValue,\n  useAnimatedStyle,\n} from \"react-native-reanimated\";\n\n/** A straight line in a square viewBox: [x1, y1, x2, y2]. */\nexport type PathSegment = [x1: number, y1: number, x2: number, y2: number];\n\nexport type DrawnPathProps = {\n  /** Straight segments drawn in order, like an SVG polyline. */\n  segments: PathSegment[];\n  /** Rendered width and height in points. */\n  size: number;\n  /** Stroke width in viewBox units. */\n  strokeWidth: number;\n  /** Side of the square viewBox the segments are written in. Defaults to 24. */\n  viewBox?: number;\n  color: ColorValue | undefined;\n  /** 0 draws nothing and 1 draws every segment, like SVG `pathLength`. */\n  progress: SharedValue<number>;\n};\n\nfunction length([x1, y1, x2, y2]: PathSegment) {\n  return Math.hypot(x2 - x1, y2 - y1);\n}\n\n/**\n * Draws a polyline on with rotated Views, so shared components need no SVG\n * library. Round caps extend half a stroke past each end, like SVG's linecap.\n */\nexport function DrawnPath({\n  segments,\n  size,\n  strokeWidth,\n  viewBox = 24,\n  color,\n  progress,\n}: DrawnPathProps) {\n  const unit = size / viewBox;\n  const stroke = strokeWidth * unit;\n  const total = segments.reduce((sum, segment) => sum + length(segment), 0);\n  let start = 0;\n\n  return (\n    <View style={{ width: size, height: size }}>\n      {segments.map((segment) => {\n        const from = start / total;\n        start += length(segment);\n        return (\n          <Stroke\n            key={segment.join()}\n            segment={segment}\n            unit={unit}\n            stroke={stroke}\n            from={from}\n            to={start / total}\n            progress={progress}\n            color={color}\n          />\n        );\n      })}\n    </View>\n  );\n}\n\nfunction Stroke({\n  segment,\n  unit,\n  stroke,\n  from,\n  to,\n  progress,\n  color,\n}: {\n  segment: PathSegment;\n  unit: number;\n  stroke: number;\n  from: number;\n  to: number;\n  progress: SharedValue<number>;\n  color: ColorValue | undefined;\n}) {\n  const [x1, y1, x2, y2] = segment;\n  const full = length(segment) * unit;\n  const angle = Math.atan2(y2 - y1, x2 - x1);\n\n  const animatedStyle = useAnimatedStyle(() => {\n    const drawn = interpolate(progress.value, [from, to], [0, 1], \"clamp\");\n    return { opacity: drawn > 0 ? 1 : 0, width: stroke + full * drawn };\n  });\n\n  return (\n    <Animated.View\n      style={[\n        styles.stroke,\n        {\n          left: x1 * unit - stroke / 2,\n          top: y1 * unit - stroke / 2,\n          height: stroke,\n          borderRadius: stroke / 2,\n          backgroundColor: color,\n          // Rotate around the start point so the stroke grows from it.\n          transformOrigin: [stroke / 2, stroke / 2, 0],\n          transform: [{ rotate: `${angle}rad` }],\n        },\n        animatedStyle,\n      ]}\n    />\n  );\n}\n\nconst styles = StyleSheet.create({\n  stroke: { position: \"absolute\" },\n});\n","type":"shared"},{"path":"src/styles/motion.ts","content":"import { Easing, ReduceMotion } from \"react-native-reanimated\";\n\n// Mirrors beUI's lib/ease.ts so native motion matches the web library.\n\nexport const EASE_OUT = Easing.bezier(0.16, 1, 0.3, 1);\nexport const EASE_IN_OUT = Easing.bezier(0.77, 0, 0.175, 1);\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\n/** Content swaps: label and icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\n/** Shared-layout glides: pills, indicators and panels moving between positions. */\nexport const SPRING_LAYOUT = {\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\n/** Dragged handles and fills; critically damped so values never rebound off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n  reduceMotion: ReduceMotion.System,\n} as const;\n\nexport const motion = {\n  press: SPRING_PRESS,\n  swap: SPRING_SWAP,\n  layout: SPRING_LAYOUT,\n  glide: SPRING_GLIDE,\n  exit: { duration: 160, easing: EASE_OUT, reduceMotion: ReduceMotion.System },\n  fade: { duration: 150, reduceMotion: ReduceMotion.System },\n} as const;\n","type":"shared"},{"path":"src/styles/theme.css","content":"/* Matches the default beUI palette in ui-components/lib/themes.ts.\n   Native surface maps to card; muted maps to muted-foreground. */\n@layer theme {\n  :root {\n    @variant light {\n      --color-background: oklch(99% 0 0);\n      --color-surface: oklch(97% 0 0);\n      --color-foreground: oklch(15% 0 0);\n      --color-muted: oklch(50% 0 0);\n      --color-border: oklch(15% 0 0 / 0.06);\n      --color-border-strong: oklch(15% 0 0 / 0.16);\n      --color-primary: #0285f7;\n      --color-on-primary: #ffffff;\n      --color-destructive: #dc2626;\n      --color-success: #059669;\n      --color-warning: #d97706;\n    }\n  }\n\n  :root {\n    @variant dark {\n      --color-background: #151515;\n      --color-surface: #1c1c1c;\n      --color-foreground: oklch(96% 0 0);\n      --color-muted: oklch(62% 0 0);\n      --color-border: rgb(255 255 255 / 0.05);\n      --color-border-strong: rgb(255 255 255 / 0.14);\n      --color-primary: #0285f7;\n      --color-on-primary: #ffffff;\n      --color-destructive: #f87171;\n      --color-success: #34d399;\n      --color-warning: #fbbf24;\n    }\n  }\n}\n","type":"theme"}]}