{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-radio-group",
  "title": "Glass Radio Group",
  "description": "Options where exactly one is chosen, with a dot that pops in.",
  "registryDependencies": [
    "@glassyui-native/glass-provider"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-radio-group.tsx",
      "content": "import { createContext, forwardRef, useCallback, useContext, useMemo } from 'react';\nimport {\n  Animated,\n  Pressable,\n  StyleSheet,\n  View,\n  type PressableProps,\n  type StyleProp,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { CheckText, CheckWell, checkFieldStyles, useDrawIn, usePressDown } from './lib/check-field';\nimport { GLASS_RADIUS } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\n\ntype RadioGroupContextValue = {\n  disabled: boolean;\n  select: (value: string) => void;\n  selected: string;\n};\n\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\n\nfunction useRadioGroup() {\n  const context = useContext(RadioGroupContext);\n\n  if (!context) {\n    throw new Error('GlassRadioGroupItem must be used within GlassRadioGroup');\n  }\n\n  return context;\n}\n\nexport type GlassRadioGroupProps = ViewProps & {\n  defaultValue?: string;\n  /** Turns every option off. */\n  disabled?: boolean;\n  onValueChange?: (value: string) => void;\n  value?: string;\n};\n\n/** A group of options where exactly one is chosen. */\nexport const GlassRadioGroup = forwardRef<View, GlassRadioGroupProps>(\n  ({ defaultValue, disabled = false, onValueChange, value, ...rest }, ref) => {\n    const [selected, select] = useControllableState(value, defaultValue ?? '', onValueChange);\n    const context = useMemo(() => ({ disabled, select, selected }), [disabled, select, selected]);\n\n    return (\n      <RadioGroupContext.Provider value={context}>\n        <View\n          ref={ref}\n          accessibilityRole=\"radiogroup\"\n          accessibilityState={{ disabled }}\n          {...rest}\n        />\n      </RadioGroupContext.Provider>\n    );\n  },\n);\n\nGlassRadioGroup.displayName = 'GlassRadioGroup';\n\nexport type GlassRadioGroupItemProps = Omit<PressableProps, 'children' | 'onPress' | 'style'> & {\n  /** A line of muted text under the label. */\n  description?: string;\n  /** Marks the choice as wrong: the edge turns the invalid colour. */\n  invalid?: boolean;\n  /** Text beside the button; the whole row then chooses it. Without one, set accessibilityLabel. */\n  label?: string;\n  style?: StyleProp<ViewStyle>;\n  value: string;\n};\n\nconst SIZE = 20;\n\n// A lone button still answers a 44-point touch.\nconst HIT_SLOP = (44 - SIZE) / 2;\n\n/** One option in a GlassRadioGroup. Its white dot springs in when chosen and shrinks straight out. */\nexport const GlassRadioGroupItem = forwardRef<View, GlassRadioGroupItemProps>(\n  (\n    { description, disabled, invalid = false, label, onPressIn, onPressOut, style, value, ...rest },\n    ref,\n  ) => {\n    const { colors, preferences } = useGlassTheme();\n    const { disabled: groupDisabled, select, selected } = useRadioGroup();\n    const checked = selected === value;\n    const off = groupDisabled || Boolean(disabled);\n    const dot = useDrawIn(checked, preferences.reduceMotion);\n    const press = usePressDown(preferences.reduceMotion, onPressIn, onPressOut);\n\n    const choose = useCallback(() => {\n      if (!checked) select(value);\n    }, [checked, select, value]);\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityHint={description}\n        accessibilityLabel={label}\n        accessibilityRole=\"radio\"\n        accessibilityState={{ checked, disabled: off }}\n        disabled={off}\n        hitSlop={label === undefined ? HIT_SLOP : undefined}\n        onPress={choose}\n        onPressIn={press.handlePressIn}\n        onPressOut={press.handlePressOut}\n        style={[\n          label === undefined ? checkFieldStyles.alone : checkFieldStyles.field,\n          off && checkFieldStyles.disabled,\n          style,\n        ]}\n        {...rest}\n      >\n        <CheckWell\n          chosen={checked}\n          invalid={invalid}\n          press={press.press}\n          radius={GLASS_RADIUS.capsule}\n          size={SIZE}\n        >\n          <Animated.View\n            style={[\n              styles.dot,\n              {\n                backgroundColor: colors.accentForeground,\n                opacity: dot.interpolate({\n                  extrapolate: 'clamp',\n                  inputRange: [0, 1],\n                  outputRange: [0, 1],\n                }),\n                transform: [{ scale: dot }],\n              },\n            ]}\n          />\n        </CheckWell>\n        {label === undefined ? null : (\n          <CheckText description={description} label={label} size=\"md\" />\n        )}\n      </Pressable>\n    );\n  },\n);\n\nGlassRadioGroupItem.displayName = 'GlassRadioGroupItem';\n\nconst styles = StyleSheet.create({\n  dot: {\n    borderRadius: GLASS_RADIUS.capsule,\n    boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.3)',\n    height: SIZE * 0.42,\n    width: SIZE * 0.42,\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-radio-group.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/check-field.tsx",
      "content": "import { useCallback, useEffect, useState, type ReactNode } from 'react';\nimport {\n  Animated,\n  Easing,\n  StyleSheet,\n  Text,\n  View,\n  type GestureResponderEvent,\n  type PressableProps,\n} from 'react-native';\nimport { useGlassTheme } from '../glass-provider';\nimport { GLASS_MOTION } from './tokens';\n\nexport type CheckSize = 'sm' | 'md';\n\n// The edge of an empty box is what says it can be ticked, so it holds 3:1 over the glass it\n// sits on, which a text field's hairline does not.\nconst EDGE = { dark: 'rgba(244, 245, 249, 0.78)', light: 'rgba(27, 29, 36, 0.6)' } as const;\n\nconst EDGE_WIDTH = 1.5;\n\n// The recessed well a field sits in, from --lg-input-shadow.\nconst WELL_SHADOW = {\n  dark: 'inset 0px 1px 2px rgba(0, 0, 0, 0.24), inset 0px -1px 0px rgba(255, 255, 255, 0.06)',\n  light: 'inset 0px 1px 2px rgba(0, 0, 0, 0.08), inset 0px -1px 0px rgba(255, 255, 255, 0.8)',\n} as const;\n\n// A chosen box is a bead of the accent, lit along its top.\nconst CHOSEN_SHADOW =\n  'inset 0px 1px 0px rgba(255, 255, 255, 0.4), inset 0px -1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px rgba(0, 0, 0, 0.28)';\n\n/**\n * Runs from 0 to 1 as `on` turns true, springing in, and back to 0, quicker and without the\n * spring, as it turns false. Reduce Motion jumps to the end.\n */\nexport function useDrawIn(on: boolean, reduceMotion: boolean) {\n  const [progress] = useState(() => new Animated.Value(on ? 1 : 0));\n\n  useEffect(() => {\n    if (reduceMotion) {\n      progress.setValue(on ? 1 : 0);\n\n      return;\n    }\n\n    if (on) {\n      Animated.spring(progress, {\n        ...GLASS_MOTION.spring,\n        toValue: 1,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n\n      return;\n    }\n\n    Animated.timing(progress, {\n      duration: 140,\n      easing: Easing.in(Easing.quad),\n      toValue: 0,\n      useNativeDriver: GLASS_MOTION.nativeDriver,\n    }).start();\n  }, [on, progress, reduceMotion]);\n\n  return progress;\n}\n\n/** A press that sets a box down and springs it back. Reduce Motion leaves the box still. */\nexport function usePressDown(\n  reduceMotion: boolean,\n  onPressIn: PressableProps['onPressIn'],\n  onPressOut: PressableProps['onPressOut'],\n) {\n  const [press] = useState(() => new Animated.Value(0));\n\n  const settle = useCallback(\n    (toValue: number) => {\n      if (reduceMotion) return;\n\n      Animated.spring(press, {\n        ...GLASS_MOTION.spring,\n        toValue,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n    },\n    [press, reduceMotion],\n  );\n\n  const handlePressIn = useCallback(\n    (event: GestureResponderEvent) => {\n      onPressIn?.(event);\n      settle(1);\n    },\n    [onPressIn, settle],\n  );\n\n  const handlePressOut = useCallback(\n    (event: GestureResponderEvent) => {\n      onPressOut?.(event);\n      settle(0);\n    },\n    [onPressOut, settle],\n  );\n\n  return { handlePressIn, handlePressOut, press };\n}\n\ntype CheckWellProps = {\n  /** The mark, centred over the box. */\n  children: ReactNode;\n  /** Filled with the accent: a checked or indeterminate box, or the chosen radio. */\n  chosen: boolean;\n  invalid: boolean;\n  /** The press progress from usePressDown. */\n  press: Animated.Value;\n  radius: number;\n  size: number;\n};\n\n/**\n * The small recessed well a checkbox or radio draws. It fills with the accent when chosen and\n * sets down to 90% under a press.\n */\nexport function CheckWell({ children, chosen, invalid, press, radius, size }: CheckWellProps) {\n  const { colors, preferences } = useGlassTheme();\n  const fill = useDrawIn(chosen, preferences.reduceMotion);\n  const corners = { borderCurve: 'continuous', borderRadius: radius } as const;\n\n  return (\n    <Animated.View\n      style={[\n        corners,\n        {\n          backgroundColor: colors.controlSurface,\n          borderColor: invalid ? colors.invalid : EDGE[preferences.theme],\n          borderWidth: EDGE_WIDTH,\n          boxShadow: WELL_SHADOW[preferences.theme],\n          height: size,\n          width: size,\n          transform: [\n            {\n              scale: preferences.reduceMotion\n                ? 1\n                : press.interpolate({ inputRange: [0, 1], outputRange: [1, 0.9] }),\n            },\n          ],\n        },\n      ]}\n    >\n      <Animated.View\n        style={[\n          styles.cover,\n          corners,\n          {\n            backgroundColor: colors.accent,\n            boxShadow: CHOSEN_SHADOW,\n            opacity: fill.interpolate({\n              extrapolate: 'clamp',\n              inputRange: [0, 1],\n              outputRange: [0, 1],\n            }),\n          },\n        ]}\n      />\n      <View style={[styles.cover, styles.centre]}>{children}</View>\n    </Animated.View>\n  );\n}\n\ntype CheckTextProps = {\n  description?: string;\n  label: string;\n  size: CheckSize;\n};\n\n/** The label beside a checkbox or radio, with an optional muted line under it. */\nexport function CheckText({ description, label, size }: CheckTextProps) {\n  const { colors } = useGlassTheme();\n\n  return (\n    <View style={styles.text}>\n      <Text\n        style={[size === 'sm' ? styles.labelSmall : styles.label, { color: colors.foreground }]}\n      >\n        {label}\n      </Text>\n      {description ? (\n        <Text style={[styles.description, { color: colors.mutedForeground }]}>{description}</Text>\n      ) : null}\n    </View>\n  );\n}\n\nexport const checkFieldStyles = StyleSheet.create({\n  alone: { alignSelf: 'flex-start' },\n  disabled: { opacity: 0.45 },\n  // A labelled row is one touch target, at least 44 points tall.\n  field: {\n    alignItems: 'flex-start',\n    flexDirection: 'row',\n    gap: 12,\n    minHeight: 44,\n    paddingVertical: 11,\n  },\n  fieldSmall: { gap: 10 },\n});\n\nconst styles = StyleSheet.create({\n  centre: { alignItems: 'center', justifyContent: 'center' },\n  // Covers the edge as well, so a chosen box shows no rim.\n  cover: {\n    bottom: -EDGE_WIDTH,\n    left: -EDGE_WIDTH,\n    position: 'absolute',\n    right: -EDGE_WIDTH,\n    top: -EDGE_WIDTH,\n  },\n  description: { fontSize: 13, lineHeight: 18 },\n  label: { fontSize: 15, lineHeight: 21 },\n  labelSmall: { fontSize: 14, lineHeight: 20 },\n  text: { flex: 1, minWidth: 0, paddingTop: 1 },\n});\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/check-field.tsx"
    }
  ],
  "type": "registry:ui"
}