{"slug":"input","name":"Input","description":"Error shake, focus ring and a drawn success check.","group":"Forms","detail_url":"https://beui.dev/native/r/input","page_url":"https://beui.dev/react-native/input","expo_go_url":"exp://u.expo.dev/93a3a95c-6dc2-4adb-b57c-efbecf859f25/--/input?runtime-version=exposdk%3A57.0.0&channel-name=production","source_url":"https://github.com/starc007/beui-native/tree/main/src/components/input","libraries":["react","react-native","react-native-reanimated","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/input/index.ts","content":"export type { InputClassNames, InputProps } from \"./input\";\nexport { Input } from \"./input\";\n","type":"component"},{"path":"src/components/input/input.tsx","content":"import { type Ref, useEffect, useRef, useState } from \"react\";\nimport {\n  AccessibilityInfo,\n  Platform,\n  StyleSheet,\n  Text,\n  TextInput,\n  type TextInputProps,\n  View,\n} from \"react-native\";\nimport Animated, {\n  Easing,\n  useAnimatedStyle,\n  useSharedValue,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { useResolveClassNames, useUniwind } from \"uniwind\";\nimport { usePresence } from \"../../hooks/use-presence\";\nimport { useReducedMotionPreference } from \"../../hooks/use-reduced-motion\";\nimport { useShake } from \"../../hooks/use-shake\";\nimport { DrawnPath, type PathSegment } from \"../../primitives/drawn-path\";\nimport { type IconSource, renderIcon } from \"../../primitives/render-icon\";\nimport { motion } from \"../../styles/motion\";\n\nconst FIELD_HEIGHT = 44;\nconst ICON = 16;\nconst CHECK: PathSegment[] = [\n  [5, 12.5, 9.5, 17],\n  [9.5, 17, 19, 7.5],\n];\nconst SHAKE = [0, -6, 6, -4, 4, -2, 0];\nconst COLOR_FADE = { duration: 200, reduceMotion: motion.fade.reduceMotion };\nconst MESSAGE_FADE = { duration: 200, reduceMotion: motion.fade.reduceMotion };\n\nexport type InputClassNames = {\n  root?: string;\n  label?: string;\n  field?: string;\n  input?: string;\n  errorMessage?: string;\n};\n\nexport type InputProps = Omit<TextInputProps, \"editable\"> & {\n  label?: string;\n  /** A truthy error shakes the field and turns it red. A string also shows a message. */\n  error?: string | boolean;\n  /** Reserves one message line so validation does not shift content below. */\n  reserveErrorLine?: boolean;\n  /** Draws a check at the trailing edge in place of `rightIcon`. */\n  success?: boolean;\n  /** Decorative icon at the leading edge, given `{ size, color }`. */\n  leftIcon?: IconSource;\n  /** Trailing icon or control, such as a show-password button. */\n  rightIcon?: IconSource;\n  disabled?: boolean;\n  /** Disables shake and transitions independently of the system preference. */\n  disableAnimation?: boolean;\n  className?: string;\n  classNames?: InputClassNames;\n  ref?: Ref<TextInput>;\n};\n\nfunction ErrorMessage({\n  message,\n  appear,\n  exiting,\n  reduce,\n  className,\n}: {\n  message: string;\n  appear: boolean;\n  exiting: boolean;\n  reduce: boolean;\n  className: string;\n}) {\n  const progress = useSharedValue(appear ? 0 : 1);\n\n  useEffect(() => {\n    progress.value = withTiming(exiting ? 0 : 1, MESSAGE_FADE);\n  }, [exiting, progress]);\n\n  const animatedStyle = useAnimatedStyle(\n    () => ({\n      opacity: progress.value,\n      transform: [{ translateY: reduce ? 0 : -4 * (1 - progress.value) }],\n    }),\n    [reduce],\n  );\n\n  return (\n    <Animated.View\n      style={[exiting && styles.exiting, animatedStyle]}\n      accessibilityLiveRegion=\"polite\"\n    >\n      <Text className={`px-1 text-xs text-destructive ${className}`}>\n        {message}\n      </Text>\n    </Animated.View>\n  );\n}\n\nfunction SuccessCheck({ reduce }: { reduce: boolean }) {\n  const { color } = useResolveClassNames(\"text-success\");\n  const draw = useSharedValue(reduce ? 1 : 0);\n\n  useEffect(() => {\n    if (!reduce) {\n      draw.value = withTiming(1, {\n        duration: 350,\n        easing: Easing.out(Easing.quad),\n      });\n    }\n  }, [draw, reduce]);\n\n  return (\n    <View\n      accessibilityElementsHidden\n      importantForAccessibility=\"no-hide-descendants\"\n      aria-hidden\n      style={styles.trailing}\n    >\n      <DrawnPath\n        segments={CHECK}\n        size={20}\n        strokeWidth={2.5}\n        color={color}\n        progress={draw}\n      />\n    </View>\n  );\n}\n\nexport function Input({\n  label,\n  error,\n  reserveErrorLine = false,\n  success = false,\n  leftIcon,\n  rightIcon,\n  disabled = false,\n  disableAnimation = false,\n  className = \"\",\n  classNames,\n  value: valueProp,\n  defaultValue,\n  onChangeText,\n  onFocus,\n  onBlur,\n  accessibilityLabel,\n  accessibilityHint,\n  keyboardAppearance,\n  style,\n  ref,\n  ...props\n}: InputProps) {\n  // Match the keyboard to the app's theme unless the caller chooses one.\n  const { theme } = useUniwind();\n  const [uncontrolled, setUncontrolled] = useState(defaultValue ?? \"\");\n  const value = valueProp ?? uncontrolled;\n  const [focused, setFocused] = useState(false);\n  const inputRef = useRef<TextInput | null>(null);\n  const reducedMotion = useReducedMotionPreference();\n  const animate = !reducedMotion && !disableAnimation;\n\n  const hasError = Boolean(error);\n  const errorMessage = typeof error === \"string\" && error ? error : null;\n  const state = hasError ? \"error\" : focused ? \"focused\" : \"idle\";\n\n  const { color: foreground } = useResolveClassNames(\"text-foreground\");\n  const { color: muted } = useResolveClassNames(\"text-muted\");\n  const { color: placeholder } = useResolveClassNames(\"text-muted/60\");\n  const { backgroundColor: idleBorder } = useResolveClassNames(\"bg-border\");\n  const { backgroundColor: focusBorder } =\n    useResolveClassNames(\"bg-foreground/40\");\n  const { backgroundColor: focusRing } =\n    useResolveClassNames(\"bg-foreground/10\");\n  const { backgroundColor: errorBorder } =\n    useResolveClassNames(\"bg-destructive\");\n  const { backgroundColor: errorRing } =\n    useResolveClassNames(\"bg-destructive/25\");\n\n  const borderColor =\n    state === \"error\"\n      ? errorBorder\n      : state === \"focused\"\n        ? focusBorder\n        : idleBorder;\n  const ringColor = state === \"error\" ? errorRing : focusRing;\n  const border = useSharedValue(String(borderColor ?? \"transparent\"));\n  const ring = useSharedValue(state === \"idle\" ? 0 : 1);\n  const { shake, style: shakeStyle } = useShake(SHAKE, 450);\n\n  useEffect(() => {\n    const next = String(borderColor ?? \"transparent\");\n    border.value = animate ? withTiming(next, COLOR_FADE) : next;\n    const target = state === \"idle\" ? 0 : 1;\n    ring.value = animate ? withTiming(target, COLOR_FADE) : target;\n  }, [animate, border, borderColor, ring, state]);\n\n  // Shake and announce when an error appears, not while it stays.\n  useEffect(() => {\n    if (!hasError) return;\n    if (animate) shake();\n  }, [animate, hasError, shake]);\n\n  useEffect(() => {\n    if (errorMessage) AccessibilityInfo.announceForAccessibility(errorMessage);\n  }, [errorMessage]);\n\n  const messages = usePresence(\n    errorMessage,\n    errorMessage ?? \"\",\n    () => MESSAGE_FADE.duration + 40,\n  );\n\n  const fieldStyle = useAnimatedStyle(() => ({ borderColor: border.value }));\n  const ringStyle = useAnimatedStyle(() => ({ opacity: ring.value }));\n\n  const trailing = success ? null : rightIcon;\n  const iconProps = { size: ICON, color: muted };\n\n  return (\n    <View className={`gap-1.5 ${className} ${classNames?.root ?? \"\"}`}>\n      {label ? (\n        <Text\n          // The field already carries the label for screen readers.\n          accessibilityElementsHidden\n          importantForAccessibility=\"no\"\n          className={`px-1 text-sm font-medium text-foreground ${classNames?.label ?? \"\"}`}\n          onPress={() => inputRef.current?.focus()}\n        >\n          {label}\n        </Text>\n      ) : null}\n\n      <Animated.View\n        style={[styles.fieldWrap, disabled && styles.disabled, shakeStyle]}\n      >\n        <Animated.View\n          style={[styles.ring, { borderColor: ringColor }, ringStyle]}\n        />\n        <Animated.View\n          className={`flex-row items-center overflow-hidden rounded-full border ${classNames?.field ?? \"\"}`}\n          style={[styles.field, fieldStyle]}\n        >\n          {leftIcon != null ? (\n            <View\n              accessibilityElementsHidden\n              importantForAccessibility=\"no-hide-descendants\"\n              aria-hidden\n              style={styles.leading}\n            >\n              {renderIcon(leftIcon, iconProps)}\n            </View>\n          ) : null}\n          <TextInput\n            {...props}\n            ref={(node) => {\n              inputRef.current = node;\n              if (typeof ref === \"function\") ref(node);\n              else if (ref) ref.current = node;\n            }}\n            value={value}\n            editable={!disabled}\n            accessibilityLabel={accessibilityLabel ?? label}\n            accessibilityHint={accessibilityHint ?? errorMessage ?? undefined}\n            aria-invalid={hasError}\n            aria-disabled={disabled}\n            placeholderTextColor={placeholder}\n            cursorColor={foreground}\n            selectionColor={foreground}\n            className={`h-full flex-1 text-foreground ${leftIcon != null ? \"ps-10\" : \"ps-3.5\"} ${trailing != null || success ? \"pe-10\" : \"pe-3.5\"} ${classNames?.input ?? \"\"}`}\n            style={[styles.input, style]}\n            keyboardAppearance={keyboardAppearance ?? theme}\n            onChangeText={(next) => {\n              if (valueProp === undefined) setUncontrolled(next);\n              onChangeText?.(next);\n            }}\n            onFocus={(event) => {\n              setFocused(true);\n              onFocus?.(event);\n            }}\n            onBlur={(event) => {\n              setFocused(false);\n              onBlur?.(event);\n            }}\n          />\n          {success ? (\n            <SuccessCheck reduce={!animate} />\n          ) : trailing != null ? (\n            <View style={styles.trailingControl}>\n              {renderIcon(trailing, iconProps)}\n            </View>\n          ) : null}\n        </Animated.View>\n      </Animated.View>\n\n      <View style={reserveErrorLine && styles.reserved}>\n        {messages.map((item) =>\n          item.value ? (\n            <ErrorMessage\n              key={item.key}\n              message={item.value}\n              appear={item.appear}\n              exiting={item.exiting}\n              reduce={!animate}\n              className={classNames?.errorMessage ?? \"\"}\n            />\n          ) : null,\n        )}\n      </View>\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  fieldWrap: { height: FIELD_HEIGHT },\n  disabled: { opacity: 0.6 },\n  // A 2-point ring around the border box, like the web's ring-2.\n  ring: {\n    position: \"absolute\",\n    top: -2,\n    bottom: -2,\n    left: -2,\n    right: -2,\n    borderRadius: FIELD_HEIGHT,\n    borderWidth: 2,\n    pointerEvents: \"none\",\n  },\n  field: { height: FIELD_HEIGHT },\n  input: {\n    // A font size without a line height: a TextInput line height pushes the\n    // text and placeholder off center on iOS.\n    fontSize: 16,\n    paddingVertical: 0,\n    // Android centers text vertically only when asked, and adds font padding.\n    textAlignVertical: \"center\",\n    includeFontPadding: false,\n    // Web inputs draw a focus outline; the field's border and ring replace it.\n    // Chrome draws an \"auto\" outline whatever its width, so the style has to\n    // go. React Native's types don't list \"none\", but React Native Web passes\n    // it through.\n    ...Platform.select({\n      web: { outlineStyle: \"none\" as \"solid\" },\n      default: {},\n    }),\n  },\n  leading: {\n    position: \"absolute\",\n    start: 12,\n    pointerEvents: \"none\",\n  },\n  trailing: {\n    position: \"absolute\",\n    end: 14,\n    pointerEvents: \"none\",\n  },\n  // A 44-point square keeps trailing controls such as a password toggle tappable.\n  trailingControl: {\n    position: \"absolute\",\n    end: 0,\n    top: 0,\n    bottom: 0,\n    width: 44,\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  },\n  reserved: { minHeight: 16 },\n  exiting: { position: \"absolute\", left: 0, right: 0, top: 0 },\n});\n","type":"component"},{"path":"src/hooks/use-presence.ts","content":"import { useEffect, useRef, useState } from \"react\";\n\nexport type PresenceItem<T> = {\n  key: string;\n  value: T;\n  /** False for the item present on first render, so it does not animate in. */\n  appear: boolean;\n  exiting: boolean;\n};\n\n/**\n * Keeps replaced items mounted while they animate out, like AnimatePresence.\n * A key that returns while exiting is revived in place.\n */\nexport function usePresence<T>(\n  key: string | null,\n  value: T,\n  exitDuration: (value: T) => number,\n) {\n  const [items, setItems] = useState<PresenceItem<T>[]>(() =>\n    key === null ? [] : [{ key, value, appear: false, exiting: false }],\n  );\n  const [currentKey, setCurrentKey] = useState(key);\n  const exitDurationRef = useRef(exitDuration);\n  exitDurationRef.current = exitDuration;\n\n  // Adjust during render so the new item and the exits start in the same frame.\n  if (key !== currentKey) {\n    setCurrentKey(key);\n    setItems((previous) => {\n      const next = previous.map((item) =>\n        item.key === key\n          ? { ...item, value, exiting: false }\n          : { ...item, exiting: true },\n      );\n      if (key !== null && !previous.some((item) => item.key === key)) {\n        next.push({ key, value, appear: true, exiting: false });\n      }\n      return next;\n    });\n  }\n\n  const exitingKeys = items\n    .filter((item) => item.exiting)\n    .map((item) => item.key)\n    .join(\"\\n\");\n\n  useEffect(() => {\n    if (!exitingKeys) return;\n    const timers = items\n      .filter((item) => item.exiting)\n      .map((item) =>\n        setTimeout(() => {\n          setItems((previous) =>\n            previous.filter(\n              (candidate) => !(candidate.key === item.key && candidate.exiting),\n            ),\n          );\n        }, exitDurationRef.current(item.value)),\n      );\n    return () => {\n      for (const timer of timers) clearTimeout(timer);\n    };\n    // Restart removal timers only when the set of exiting items changes.\n  }, [exitingKeys]);\n\n  return items;\n}\n","type":"shared"},{"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/hooks/use-shake.ts","content":"import { useCallback } from \"react\";\nimport {\n  type EasingFunction,\n  type EasingFunctionFactory,\n  useAnimatedStyle,\n  useSharedValue,\n  withSequence,\n  withTiming,\n} from \"react-native-reanimated\";\n\n/**\n * Horizontal shake through `keyframes` (in points) over `duration` ms, split\n * evenly like a Motion keyframe array. Callers skip it under reduced motion.\n */\nexport function useShake(\n  keyframes: number[],\n  duration: number,\n  easing?: EasingFunction | EasingFunctionFactory,\n) {\n  const offset = useSharedValue(0);\n  const step = duration / Math.max(1, keyframes.length - 1);\n\n  const shake = useCallback(() => {\n    const [first = 0, ...rest] = keyframes;\n    offset.value = first;\n    offset.value = withSequence(\n      // An explicit `easing: undefined` would replace Reanimated's default.\n      ...rest.map((point) =>\n        withTiming(\n          point,\n          easing ? { duration: step, easing } : { duration: step },\n        ),\n      ),\n    );\n  }, [easing, keyframes, offset, step]);\n\n  const style = useAnimatedStyle(() => ({\n    transform: [{ translateX: offset.value }],\n  }));\n\n  return { shake, style };\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/primitives/render-icon.tsx","content":"import {\n  type ComponentType,\n  createElement,\n  isValidElement,\n  type ReactNode,\n} from \"react\";\nimport type { ColorValue } from \"react-native\";\n\nexport type IconProps = { size: number; color: ColorValue | undefined };\n/** A component receiving `{ size, color }` (such as a lucide icon), or a prebuilt node. */\nexport type IconSource = ComponentType<IconProps> | ReactNode;\n\nexport function renderIcon(icon: IconSource, props: IconProps) {\n  if (typeof icon === \"function\") return createElement(icon, props);\n  // forwardRef and memo components (lucide-react-native, for example) are objects.\n  if (\n    icon != null &&\n    typeof icon === \"object\" &&\n    !isValidElement(icon) &&\n    \"$$typeof\" in icon\n  ) {\n    return createElement(icon as unknown as ComponentType<IconProps>, props);\n  }\n  return icon;\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"}]}