{"slug":"animated-badge","name":"Animated Badge","description":"Status pills whose icon and label roll.","group":"Motion","detail_url":"https://beui.dev/native/r/animated-badge","page_url":"https://beui.dev/react-native/animated-badge","expo_go_url":"exp://u.expo.dev/93a3a95c-6dc2-4adb-b57c-efbecf859f25/--/animated-badge?runtime-version=exposdk%3A57.0.0&channel-name=production","source_url":"https://github.com/starc007/beui-native/tree/main/src/components/animated-badge","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/animated-badge/animated-badge.tsx","content":"import { type ReactNode, type Ref, useEffect, useRef, useState } from \"react\";\nimport {\n  type ColorValue,\n  StyleSheet,\n  View,\n  type ViewProps,\n} from \"react-native\";\nimport Animated, {\n  cancelAnimation,\n  Easing,\n  interpolateColor,\n  useAnimatedStyle,\n  useSharedValue,\n  withRepeat,\n  withSequence,\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 { type IconSource, renderIcon } from \"../../primitives/render-icon\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"../../styles/motion\";\n\nexport type AnimatedBadgeStatus =\n  | \"neutral\"\n  | \"info\"\n  | \"success\"\n  | \"warning\"\n  | \"danger\"\n  | \"loading\";\n\nexport type AnimatedBadgeSize = \"sm\" | \"md\";\n\nexport type AnimatedBadgeProps = Omit<ViewProps, \"children\"> & {\n  /** Defaults to \"neutral\". */\n  status?: AnimatedBadgeStatus;\n  /** Defaults to \"md\". */\n  size?: AnimatedBadgeSize;\n  /** The label. Strings and numbers roll when they change. */\n  children?: ReactNode;\n  /** Replaces the status icon: a component given `{ size, color }`, or an element. */\n  icon?: IconSource;\n  /** Defaults to true. */\n  showIcon?: boolean;\n  /** A slow breathing fill. Defaults to true while loading. */\n  pulse?: boolean;\n  /** Rolls the label when this changes. Defaults to a string or number label, then the status. */\n  contentKey?: string | number;\n  /** Swaps without rolling, independently of the system preference. */\n  disableAnimation?: boolean;\n  className?: string;\n  ref?: Ref<View>;\n};\n\n// Full class strings, so Uniwind can see every token at build time.\nconst STATUS_CLASS: Record<AnimatedBadgeStatus, string> = {\n  neutral: \"border-border bg-surface text-muted\",\n  info: \"border-primary/30 bg-surface text-primary\",\n  success: \"border-success/30 bg-success/10 text-success\",\n  warning: \"border-warning/30 bg-warning/10 text-warning\",\n  danger: \"border-destructive/30 bg-destructive/10 text-destructive\",\n  loading: \"border-primary/30 bg-surface text-primary\",\n};\n\nconst SIZES = {\n  sm: { height: 24, gap: 6, padding: 8, font: 11, icon: 12 },\n  md: { height: 32, gap: 8, padding: 12, font: 12, icon: 14 },\n} as const;\n\n// The leaving content outruns the entering one, as on the web.\nconst ROLL_OUT = { duration: 140, easing: EASE_OUT };\nconst COLOR_FADE = { duration: 300, easing: EASE_OUT };\nconst PULSE = { duration: 800, easing: Easing.inOut(Easing.ease) };\nconst SPIN = { duration: 1000, easing: Easing.linear };\n\nconst GLYPHS: Partial<Record<AnimatedBadgeStatus, PathSegment[]>> = {\n  success: [\n    [20, 6, 9, 17],\n    [9, 17, 4, 12],\n  ],\n  danger: [\n    [18, 6, 6, 18],\n    [6, 6, 18, 18],\n  ],\n  info: [\n    [12, 16, 12, 12],\n    [12, 8, 12, 8.01],\n  ],\n  warning: [\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};\n\nfunction StatusIcon({\n  status,\n  size,\n  color,\n  reduce,\n}: {\n  status: AnimatedBadgeStatus;\n  size: number;\n  color: ColorValue | undefined;\n  reduce: boolean;\n}) {\n  const turn = useSharedValue(0);\n  // Status glyphs are drawn whole; DrawnPath's progress is for drawing on.\n  const drawn = useSharedValue(1);\n  const spinning = status === \"loading\" && !reduce;\n\n  useEffect(() => {\n    if (!spinning) return;\n    turn.value = withRepeat(withTiming(1, SPIN), -1, false);\n    return () => cancelAnimation(turn);\n  }, [spinning, turn]);\n\n  const spinStyle = useAnimatedStyle(() => ({\n    transform: [{ rotate: `${turn.value * 360}deg` }],\n  }));\n\n  const unit = size / 24;\n  const ring = {\n    width: 18 * unit,\n    height: 18 * unit,\n    borderRadius: 9 * unit,\n    borderWidth: 2 * unit,\n    borderColor: color,\n  };\n\n  if (status === \"neutral\") {\n    return (\n      <View style={[styles.icon, { width: size, height: size }]}>\n        <View style={ring} />\n      </View>\n    );\n  }\n  if (status === \"loading\") {\n    // Three quarters of a ring, like an open loader circle.\n    return (\n      <Animated.View\n        style={[styles.icon, { width: size, height: size }, spinStyle]}\n      >\n        <View style={[ring, { borderTopColor: \"transparent\" }]} />\n      </Animated.View>\n    );\n  }\n  const glyph = (\n    <DrawnPath\n      segments={GLYPHS[status] ?? []}\n      size={size}\n      strokeWidth={2.4}\n      color={color}\n      progress={drawn}\n    />\n  );\n  if (status === \"info\") {\n    return (\n      <View style={[styles.icon, { width: size, height: size }]}>\n        <View\n          style={[\n            ring,\n            styles.overlay,\n            { width: 20 * unit, height: 20 * unit, borderRadius: 10 * unit },\n          ]}\n        />\n        {glyph}\n      </View>\n    );\n  }\n  return glyph;\n}\n\ntype Layer = { key: string | number; node: ReactNode; leaving: boolean };\n\n/**\n * Rolls its content: the leaving layer slides up and out while the next slides\n * in from below. The slot takes the size of the entering content.\n */\nfunction Roll({\n  rollKey,\n  distance,\n  reduce,\n  children,\n}: {\n  rollKey: string | number;\n  distance: number;\n  reduce: boolean;\n  children: ReactNode;\n}) {\n  const [layers, setLayers] = useState<Layer[]>([]);\n  const [shownKey, setShownKey] = useState(rollKey);\n  const lastNode = useRef<ReactNode>(children);\n  // Content on the badge's first render lands at rest, like initial={false}.\n  const mounted = useRef(false);\n  useEffect(() => {\n    mounted.current = true;\n  }, []);\n\n  // A new key turns the content on screen into a leaving layer.\n  if (rollKey !== shownKey) {\n    const leavingKey = shownKey;\n    const leavingNode = lastNode.current;\n    setShownKey(rollKey);\n    setLayers((current) =>\n      reduce\n        ? []\n        : [\n            ...current.filter((layer) => layer.key !== rollKey),\n            { key: leavingKey, node: leavingNode, leaving: true },\n          ],\n    );\n  }\n\n  useEffect(() => {\n    lastNode.current = children;\n  });\n\n  return (\n    <View>\n      {layers.map((layer) => (\n        <RollLayer\n          key={`out-${layer.key}`}\n          distance={distance}\n          leaving\n          onDone={() =>\n            setLayers((current) => current.filter((item) => item !== layer))\n          }\n        >\n          {layer.node}\n        </RollLayer>\n      ))}\n      <RollLayer\n        key={`in-${rollKey}`}\n        distance={distance}\n        animate={!reduce && mounted.current}\n      >\n        {children}\n      </RollLayer>\n    </View>\n  );\n}\n\nfunction RollLayer({\n  distance,\n  leaving = false,\n  animate = true,\n  onDone,\n  children,\n}: {\n  distance: number;\n  leaving?: boolean;\n  animate?: boolean;\n  onDone?: () => void;\n  children: ReactNode;\n}) {\n  const first = useRef(true);\n  const enter = !leaving && animate;\n  const offset = useSharedValue(enter ? distance : 0);\n  const opacity = useSharedValue(enter ? 0 : 1);\n\n  useEffect(() => {\n    if (!first.current) return;\n    first.current = false;\n    if (leaving) {\n      offset.value = withTiming(-distance, ROLL_OUT);\n      opacity.value = withTiming(0, ROLL_OUT, (finished) => {\n        if (finished && onDone) scheduleOnRN(onDone);\n      });\n    } else if (enter) {\n      offset.value = withSpring(0, SPRING_SWAP);\n      opacity.value = withSpring(1, SPRING_SWAP);\n    }\n  }, [distance, enter, leaving, offset, onDone, opacity]);\n\n  const style = useAnimatedStyle(() => ({\n    opacity: opacity.value,\n    transform: [{ translateY: offset.value }],\n  }));\n\n  return (\n    <Animated.View\n      accessibilityElementsHidden={leaving}\n      importantForAccessibility={leaving ? \"no-hide-descendants\" : \"auto\"}\n      style={[leaving ? styles.leaving : null, style]}\n    >\n      {children}\n    </Animated.View>\n  );\n}\n\n/** A status pill whose icon and label roll when they change. */\nexport function AnimatedBadge({\n  status = \"neutral\",\n  size = \"md\",\n  children,\n  icon,\n  showIcon = true,\n  pulse = status === \"loading\",\n  contentKey,\n  disableAnimation = false,\n  className = \"\",\n  style,\n  accessibilityLabel,\n  ref,\n  ...props\n}: AnimatedBadgeProps) {\n  const reducedMotion = useReducedMotionPreference();\n  const reduce = reducedMotion || disableAnimation;\n  const metrics = SIZES[size];\n  const colors = useResolveClassNames(STATUS_CLASS[status]);\n\n  // 0 is a real label; booleans and \"\" from conditional callers are not.\n  const hasLabel =\n    children != null && typeof children !== \"boolean\" && children !== \"\";\n  const labelKey =\n    contentKey ??\n    (typeof children === \"string\" || typeof children === \"number\"\n      ? children\n      : status);\n\n  // Colors blend from the previous status over 300 ms. On web the border\n  // resolves per side, so borderTopColor stands in for borderColor.\n  const bg = String(colors.backgroundColor ?? \"transparent\");\n  const border = String(\n    colors.borderColor ?? colors.borderTopColor ?? \"transparent\",\n  );\n  const shown = useRef({ bg, border });\n  const blend = useSharedValue(1);\n  const palette = useSharedValue({\n    fromBg: bg,\n    toBg: bg,\n    fromBorder: border,\n    toBorder: border,\n  });\n  useEffect(() => {\n    const previous = shown.current;\n    if (previous.bg === bg && previous.border === border) return;\n    shown.current = { bg, border };\n    palette.value = {\n      fromBg: previous.bg,\n      toBg: bg,\n      fromBorder: previous.border,\n      toBorder: border,\n    };\n    blend.value = 0;\n    blend.value = reduce ? 1 : withTiming(1, COLOR_FADE);\n  }, [bg, blend, border, palette, reduce]);\n\n  // The pill springs to its content's natural width.\n  const width = useSharedValue(-1);\n  const measured = (next: number) => {\n    if (width.value < 0 || reduce) width.value = next;\n    else width.value = withSpring(next, SPRING_LAYOUT);\n  };\n\n  const pillStyle = useAnimatedStyle(() => {\n    const p = palette.value;\n    return {\n      width: width.value < 0 ? undefined : width.value,\n      backgroundColor: interpolateColor(\n        blend.value,\n        [0, 1],\n        [p.fromBg, p.toBg],\n      ),\n      borderColor: interpolateColor(\n        blend.value,\n        [0, 1],\n        [p.fromBorder, p.toBorder],\n      ),\n    };\n  });\n\n  const breath = useSharedValue(0.06);\n  const breathing = pulse && !reduce;\n  useEffect(() => {\n    if (!breathing) {\n      breath.value = 0;\n      return;\n    }\n    breath.value = 0.06;\n    breath.value = withRepeat(\n      withSequence(withTiming(0.18, PULSE), withTiming(0.06, PULSE)),\n      -1,\n    );\n    return () => cancelAnimation(breath);\n  }, [breath, breathing]);\n  const pulseStyle = useAnimatedStyle(() => ({ opacity: breath.value }));\n\n  const textColor = colors.color;\n  const label =\n    typeof children === \"string\" || typeof children === \"number\"\n      ? String(children)\n      : undefined;\n  const lineHeight = Math.round(metrics.font * 1.4);\n\n  return (\n    <Animated.View\n      {...props}\n      ref={ref}\n      accessible\n      accessibilityRole=\"text\"\n      accessibilityLabel={accessibilityLabel ?? label}\n      // Hugs its content unless the caller places it with a self-* class.\n      className={`overflow-hidden rounded-full border ${/\\bself-/.test(className) ? \"\" : \"self-start\"} ${className}`}\n      style={[{ minHeight: metrics.height }, pillStyle, style]}\n    >\n      {breathing ? (\n        <Animated.View\n          style={[\n            StyleSheet.absoluteFill,\n            styles.passThrough,\n            { backgroundColor: textColor },\n            pulseStyle,\n          ]}\n        />\n      ) : null}\n      <View\n        style={[\n          styles.row,\n          {\n            minHeight: metrics.height - 2,\n            gap: hasLabel && showIcon ? metrics.gap : 0,\n            paddingHorizontal: metrics.padding - 1,\n          },\n        ]}\n        onLayout={(event) => measured(event.nativeEvent.layout.width + 2)}\n      >\n        {showIcon ? (\n          <Roll rollKey={status} distance={12} reduce={reduce}>\n            <View style={{ width: metrics.icon, height: metrics.icon }}>\n              {icon != null ? (\n                renderIcon(icon, {\n                  size: metrics.icon,\n                  color: textColor as string,\n                })\n              ) : (\n                <StatusIcon\n                  status={status}\n                  size={metrics.icon}\n                  color={textColor}\n                  reduce={reduce}\n                />\n              )}\n            </View>\n          </Roll>\n        ) : null}\n        {hasLabel ? (\n          <Roll rollKey={labelKey} distance={lineHeight * 0.9} reduce={reduce}>\n            <Animated.Text\n              numberOfLines={1}\n              style={{\n                color: textColor,\n                fontSize: metrics.font,\n                lineHeight,\n                fontWeight: \"500\",\n                fontVariant: [\"tabular-nums\"],\n              }}\n            >\n              {children}\n            </Animated.Text>\n          </Roll>\n        ) : null}\n      </View>\n    </Animated.View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  row: {\n    flexDirection: \"row\",\n    alignItems: \"center\",\n    alignSelf: \"flex-start\",\n    flexShrink: 0,\n  },\n  icon: { alignItems: \"center\", justifyContent: \"center\" },\n  overlay: { position: \"absolute\" },\n  leaving: { position: \"absolute\", top: 0, left: 0 },\n  passThrough: { pointerEvents: \"none\" },\n});\n","type":"component"},{"path":"src/components/animated-badge/index.ts","content":"export type {\n  AnimatedBadgeProps,\n  AnimatedBadgeSize,\n  AnimatedBadgeStatus,\n} from \"./animated-badge\";\nexport { AnimatedBadge } from \"./animated-badge\";\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/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"}]}