{"slug":"select","name":"Select","description":"A panel that unfolds out of its trigger.","group":"Forms","detail_url":"https://beui.dev/native/r/select","page_url":"https://beui.dev/react-native/select","expo_go_url":"exp://u.expo.dev/93a3a95c-6dc2-4adb-b57c-efbecf859f25/--/select?runtime-version=exposdk%3A57.0.0&channel-name=production","source_url":"https://github.com/starc007/beui-native/tree/main/src/components/select","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/select/index.ts","content":"export type {\n  SelectContentProps,\n  SelectItemProps,\n  SelectProps,\n  SelectTriggerProps,\n  SelectValueProps,\n} from \"./select\";\nexport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"./select\";\n","type":"component"},{"path":"src/components/select/select.tsx","content":"import {\n  createContext,\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useContext,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  Modal,\n  Pressable,\n  ScrollView,\n  StyleSheet,\n  Text,\n  useWindowDimensions,\n  View,\n  type ViewProps,\n} from \"react-native\";\nimport Animated, {\n  cancelAnimation,\n  interpolate,\n  ReduceMotion,\n  type SharedValue,\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 } from \"../../styles/motion\";\n\n// One progress value drives the surface, gap, corners and chevron, as on the\n// web: a short, barely bouncy spring in, and a quicker ease back out.\nconst UNFOLD = {\n  duration: 300,\n  dampingRatio: 0.88,\n  reduceMotion: ReduceMotion.System,\n};\nconst FOLD = { duration: 200, easing: EASE_OUT };\nconst FADE = { duration: 120 };\nconst ITEMS_IN = { duration: 120, easing: EASE_OUT };\nconst RADIUS = 12;\n// How far the open panel sits from the trigger once it has pulled away.\nconst GAP = 8;\n// The panel starts as a thin lip flush against the trigger's edge.\nconst LIP = 8;\n// Space to keep between the panel and the window edge when choosing a side.\nconst EDGE = 16;\nconst MAX_HEIGHT = 320;\nconst CHEVRON: PathSegment[] = [\n  [6, 9, 12, 15],\n  [12, 15, 18, 9],\n];\nconst CHECK: PathSegment[] = [\n  [5, 13, 9, 17],\n  [9, 17, 19, 7],\n];\n\ntype Placement = \"bottom\" | \"top\";\ntype Anchor = { x: number; y: number; width: number; height: number };\n\ntype SelectContextValue = {\n  value: string | undefined;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  select: (value: string) => void;\n  labels: Map<string, string>;\n  register: (value: string, label: string) => void;\n  progress: SharedValue<number>;\n  reduce: boolean;\n  disabled: boolean;\n  triggerRef: RefObject<View | null>;\n  anchor: Anchor | null;\n  placement: Placement;\n  setPlacement: (placement: Placement) => void;\n};\n\nconst SelectContext = createContext<SelectContextValue | null>(null);\n// Items render twice: once hidden, only to register their labels for\n// SelectValue, and once in the open panel.\nconst RegistrationContext = createContext(false);\n\nfunction useSelect(component: string) {\n  const context = useContext(SelectContext);\n  if (!context) throw new Error(`${component} must be used within Select.`);\n  return context;\n}\n\nexport type SelectProps = {\n  /** Controlled value. Omit it and use `defaultValue` for uncontrolled use. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Controlled open state. Omit it and use `defaultOpen` for uncontrolled use. */\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  disabled?: boolean;\n  /** Opens and closes without unfolding, independently of the system preference. */\n  disableAnimation?: boolean;\n  children: ReactNode;\n};\n\nexport function Select({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  disabled = false,\n  disableAnimation = false,\n  children,\n}: SelectProps) {\n  const reducedMotion = useReducedMotionPreference();\n  const reduce = reducedMotion || disableAnimation;\n  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n  const value = valueProp ?? uncontrolledValue;\n  const open = openProp ?? uncontrolledOpen;\n  const [labels, setLabels] = useState<Map<string, string>>(new Map());\n  const [anchor, setAnchor] = useState<Anchor | null>(null);\n  const [placement, setPlacement] = useState<Placement>(\"bottom\");\n  const triggerRef = useRef<View>(null);\n  const progress = useSharedValue(0);\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (next && disabled) return;\n      if (next) {\n        // Find the trigger on screen before the panel appears beside it.\n        triggerRef.current?.measureInWindow((x, y, width, height) => {\n          setAnchor({ x, y, width, height });\n          if (openProp === undefined) setUncontrolledOpen(true);\n          onOpenChange?.(true);\n        });\n        return;\n      }\n      if (openProp === undefined) setUncontrolledOpen(false);\n      onOpenChange?.(false);\n    },\n    [disabled, onOpenChange, openProp],\n  );\n\n  // A controlled open without a measured trigger still needs its anchor.\n  useEffect(() => {\n    if (!open || anchor) return;\n    triggerRef.current?.measureInWindow((x, y, width, height) =>\n      setAnchor({ x, y, width, height }),\n    );\n  }, [anchor, open]);\n\n  const select = useCallback(\n    (next: string) => {\n      if (valueProp === undefined) setUncontrolledValue(next);\n      onValueChange?.(next);\n      setOpen(false);\n    },\n    [onValueChange, setOpen, valueProp],\n  );\n\n  const register = useCallback((item: string, label: string) => {\n    setLabels((current) =>\n      current.get(item) === label ? current : new Map(current).set(item, label),\n    );\n  }, []);\n\n  const context = useMemo<SelectContextValue>(\n    () => ({\n      value,\n      open,\n      setOpen,\n      select,\n      labels,\n      register,\n      progress,\n      reduce,\n      disabled,\n      triggerRef,\n      anchor,\n      placement,\n      setPlacement,\n    }),\n    [\n      anchor,\n      disabled,\n      labels,\n      open,\n      placement,\n      progress,\n      reduce,\n      register,\n      select,\n      setOpen,\n      value,\n    ],\n  );\n\n  return (\n    <SelectContext.Provider value={context}>{children}</SelectContext.Provider>\n  );\n}\n\nexport type SelectTriggerProps = Omit<ViewProps, \"children\"> & {\n  children: ReactNode;\n  className?: string;\n};\n\nexport function SelectTrigger({\n  children,\n  className = \"\",\n  accessibilityLabel,\n  accessibilityHint,\n  ...props\n}: SelectTriggerProps) {\n  const context = useSelect(\"SelectTrigger\");\n  const { progress, reduce, placement, open, disabled } = context;\n  const { color: muted } = useResolveClassNames(\"text-muted\");\n  const drawn = useSharedValue(1);\n  const label = context.labels.get(context.value ?? \"\");\n\n  // The edge facing the panel flattens while the panel is attached to it and\n  // rounds back as the panel pulls away, on the panel's own curve.\n  const shapeStyle = useAnimatedStyle(() => {\n    const near = reduce\n      ? RADIUS\n      : interpolate(\n          progress.value,\n          [0, 0.2, 0.4, 1],\n          [RADIUS, 0, 0, RADIUS],\n          \"clamp\",\n        );\n    return placement === \"top\"\n      ? {\n          borderTopLeftRadius: near,\n          borderTopRightRadius: near,\n          borderBottomLeftRadius: RADIUS,\n          borderBottomRightRadius: RADIUS,\n        }\n      : {\n          borderTopLeftRadius: RADIUS,\n          borderTopRightRadius: RADIUS,\n          borderBottomLeftRadius: near,\n          borderBottomRightRadius: near,\n        };\n  }, [placement, reduce]);\n  const chevronStyle = useAnimatedStyle(() => ({\n    transform: [\n      {\n        rotate: `${Math.min(1, Math.max(0, progress.value)) * 180}deg`,\n      },\n    ],\n  }));\n\n  return (\n    <Animated.View\n      {...props}\n      ref={context.triggerRef}\n      collapsable={false}\n      className={`border border-border bg-background ${disabled ? \"opacity-50\" : \"\"} ${className}`}\n      style={[styles.trigger, shapeStyle, props.style]}\n    >\n      <Pressable\n        accessibilityRole=\"combobox\"\n        accessibilityLabel={accessibilityLabel}\n        accessibilityHint={accessibilityHint}\n        accessibilityState={{ expanded: open, disabled }}\n        accessibilityValue={label ? { text: label } : undefined}\n        disabled={disabled}\n        onPress={() => context.setOpen(!open)}\n        className=\"min-h-11 flex-row items-center justify-between gap-2 px-3 py-2\"\n      >\n        <View className=\"flex-1\">{children}</View>\n        <Animated.View style={[styles.chevron, chevronStyle]}>\n          <DrawnPath\n            segments={CHEVRON}\n            size={16}\n            strokeWidth={2}\n            color={muted}\n            progress={drawn}\n          />\n        </Animated.View>\n      </Pressable>\n    </Animated.View>\n  );\n}\n\nexport type SelectValueProps = {\n  placeholder?: string;\n  className?: string;\n};\n\nexport function SelectValue({\n  placeholder = \"Select\",\n  className = \"\",\n}: SelectValueProps) {\n  const context = useSelect(\"SelectValue\");\n  const label =\n    context.value === undefined ? undefined : context.labels.get(context.value);\n  return (\n    <Text\n      numberOfLines={1}\n      className={`text-sm ${label ? \"text-foreground\" : \"text-muted\"} ${className}`}\n    >\n      {label ?? placeholder}\n    </Text>\n  );\n}\n\nexport type SelectContentProps = {\n  children: ReactNode;\n  className?: string;\n};\n\nexport function SelectContent({\n  children,\n  className = \"\",\n}: SelectContentProps) {\n  const context = useSelect(\"SelectContent\");\n  const { open, progress, reduce, anchor, placement, setPlacement } = context;\n  const window = useWindowDimensions();\n  const [height, setHeight] = useState(0);\n  // The modal stays up while the panel folds away.\n  const [present, setPresent] = useState(open);\n  const itemsIn = useSharedValue(open ? 1 : 0);\n\n  if (open && !present) setPresent(true);\n\n  const hide = useCallback(() => setPresent(false), []);\n\n  useEffect(() => {\n    if (!present) return;\n    cancelAnimation(progress);\n    if (open) {\n      progress.value = reduce ? withTiming(1, FADE) : withSpring(1, UNFOLD);\n      itemsIn.value = reduce ? 1 : withTiming(1, ITEMS_IN);\n    } else {\n      itemsIn.value = withTiming(0, { duration: 80 });\n      progress.value = withTiming(0, reduce ? FADE : FOLD, (done) => {\n        if (done) scheduleOnRN(hide);\n      });\n    }\n  }, [hide, itemsIn, open, present, progress, reduce]);\n\n  // Choose a side once the panel's height is known: below unless it doesn't\n  // fit there and there is more room above.\n  useLayoutEffect(() => {\n    if (!open || !anchor || height === 0) return;\n    const below = window.height - (anchor.y + anchor.height);\n    const above = anchor.y;\n    setPlacement(\n      below < height + GAP + EDGE && above > below ? \"top\" : \"bottom\",\n    );\n  }, [anchor, height, open, setPlacement, window.height]);\n\n  const panelHeight = Math.min(height, MAX_HEIGHT);\n  const isTop = placement === \"top\";\n  const ready = panelHeight > 0 && !reduce;\n\n  const panelStyle = useAnimatedStyle(() => {\n    const p = progress.value;\n    const reveal = Math.min(1, Math.max(0, p));\n    const detach = interpolate(p, [0.4, 1], [0, 1], \"clamp\");\n    const near = ready ? RADIUS * detach : RADIUS;\n    // Reveal a full-size panel from its near edge without scaling its rows.\n    const visible = ready ? LIP + (panelHeight - LIP) * reveal : panelHeight;\n    return {\n      height: visible,\n      opacity: interpolate(p, reduce ? [0, 1] : [0, 0.12], [0, 1], \"clamp\"),\n      transform: [\n        { translateY: ready ? (isTop ? GAP : -GAP) * (1 - detach) : 0 },\n      ],\n      borderTopLeftRadius: isTop ? RADIUS : near,\n      borderTopRightRadius: isTop ? RADIUS : near,\n      borderBottomLeftRadius: isTop ? near : RADIUS,\n      borderBottomRightRadius: isTop ? near : RADIUS,\n    };\n  }, [isTop, panelHeight, ready, reduce]);\n\n  const itemsStyle = useAnimatedStyle(\n    () => ({\n      opacity: itemsIn.value,\n      transform: [{ translateY: reduce ? 0 : -4 * (1 - itemsIn.value) }],\n    }),\n    [reduce],\n  );\n\n  const position =\n    anchor === null\n      ? null\n      : isTop\n        ? {\n            left: anchor.x,\n            width: anchor.width,\n            bottom: window.height - anchor.y + GAP,\n          }\n        : {\n            left: anchor.x,\n            width: anchor.width,\n            top: anchor.y + anchor.height + GAP,\n          };\n\n  return (\n    <>\n      {/* Hidden copy so SelectValue knows every label before the first open. */}\n      <View style={styles.registry} accessibilityElementsHidden>\n        <RegistrationContext.Provider value={true}>\n          {children}\n        </RegistrationContext.Provider>\n      </View>\n\n      <Modal\n        transparent\n        visible={present}\n        animationType=\"none\"\n        statusBarTranslucent\n        navigationBarTranslucent\n        onRequestClose={() => context.setOpen(false)}\n      >\n        <Pressable\n          accessibilityLabel=\"Close\"\n          accessibilityRole=\"button\"\n          style={StyleSheet.absoluteFill}\n          onPress={() => context.setOpen(false)}\n        />\n        {position ? (\n          <Animated.View\n            accessibilityViewIsModal\n            className={`overflow-hidden border border-border bg-background ${className}`}\n            style={[styles.panel, position, panelStyle]}\n          >\n            {/* Pinned to the near edge, so the reveal uncovers it from there. */}\n            <View\n              style={[\n                styles.content,\n                isTop ? styles.pinBottom : styles.pinTop,\n                height > MAX_HEIGHT && { height: MAX_HEIGHT },\n              ]}\n            >\n              <ScrollView\n                scrollEnabled={height > MAX_HEIGHT}\n                bounces={false}\n                onContentSizeChange={(_, contentHeight) => {\n                  // Content plus the panel's 1-point borders.\n                  setHeight(contentHeight + 2);\n                }}\n              >\n                <Animated.View style={[styles.items, itemsStyle]}>\n                  {children}\n                </Animated.View>\n              </ScrollView>\n            </View>\n          </Animated.View>\n        ) : null}\n      </Modal>\n    </>\n  );\n}\n\nexport type SelectItemProps = {\n  value: string;\n  /** Text for SelectValue and screen readers when children aren't a string. */\n  label?: string;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n};\n\nexport function SelectItem({\n  value,\n  label: labelProp,\n  disabled = false,\n  className = \"\",\n  children,\n}: SelectItemProps) {\n  const context = useSelect(\"SelectItem\");\n  const registering = useContext(RegistrationContext);\n  const { color: foreground } = useResolveClassNames(\"text-foreground\");\n  const drawn = useSharedValue(1);\n  const selected = context.value === value;\n  const label = labelProp ?? (typeof children === \"string\" ? children : value);\n  const { register } = context;\n\n  useLayoutEffect(() => {\n    if (registering) register(value, label);\n  }, [label, register, registering, value]);\n\n  if (registering) return null;\n\n  return (\n    <Pressable\n      accessibilityRole=\"button\"\n      accessibilityLabel={label}\n      accessibilityState={{ selected, disabled }}\n      disabled={disabled}\n      onPress={() => context.select(value)}\n    >\n      {({ pressed }) => (\n        <View\n          className={`min-h-10 flex-row items-center justify-between gap-2 rounded-lg px-2.5 py-2 ${selected || pressed ? \"bg-surface\" : \"\"} ${disabled ? \"opacity-50\" : \"\"} ${className}`}\n        >\n          {typeof children === \"string\" ? (\n            <Text\n              className={`flex-1 text-sm ${selected ? \"text-foreground\" : \"text-muted\"}`}\n            >\n              {children}\n            </Text>\n          ) : (\n            <View className=\"flex-1\">{children}</View>\n          )}\n          {selected ? (\n            <DrawnPath\n              segments={CHECK}\n              size={14}\n              strokeWidth={2.5}\n              color={foreground}\n              progress={drawn}\n            />\n          ) : null}\n        </View>\n      )}\n    </Pressable>\n  );\n}\n\nconst styles = StyleSheet.create({\n  trigger: { alignSelf: \"stretch\" },\n  chevron: {\n    width: 20,\n    height: 20,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  panel: {\n    position: \"absolute\",\n    boxShadow:\n      \"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)\",\n  },\n  content: { position: \"absolute\", left: 0, right: 0 },\n  pinTop: { top: 0 },\n  pinBottom: { bottom: 0 },\n  items: { padding: 4 },\n  registry: { display: \"none\" },\n});\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"}]}