{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-checkbox",
  "title": "Glass Checkbox",
  "description": "A glass checkbox whose mark draws itself in, with an indeterminate state.",
  "dependencies": [
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-provider"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-checkbox.tsx",
      "content": "import { forwardRef, useCallback } from 'react';\nimport {\n  Animated,\n  Pressable,\n  StyleSheet,\n  type PressableProps,\n  type StyleProp,\n  type View,\n  type ViewStyle,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport {\n  CheckText,\n  CheckWell,\n  checkFieldStyles,\n  useDrawIn,\n  usePressDown,\n  type CheckSize,\n} from './lib/check-field';\nimport { GlassIcon, type GlassIconName } from './lib/icons';\nimport { useControllableState } from './lib/use-controllable-state';\n\nexport type GlassCheckboxSize = CheckSize;\n\nexport type GlassCheckedState = boolean | 'indeterminate';\n\nexport type GlassCheckboxProps = Omit<PressableProps, 'children' | 'onPress' | 'style'> & {\n  checked?: GlassCheckedState;\n  defaultChecked?: GlassCheckedState;\n  /** A line of muted text under the label. */\n  description?: string;\n  /** Marks the choice as wrong: the box's edge turns the invalid colour. */\n  invalid?: boolean;\n  /** Text beside the box; the whole row then toggles it. Without one, set accessibilityLabel. */\n  label?: string;\n  /** Hears the new state. Pressing an indeterminate box checks it. */\n  onCheckedChange?: (checked: boolean) => void;\n  size?: GlassCheckboxSize;\n  style?: StyleProp<ViewStyle>;\n};\n\nconst BOX = {\n  sm: { radius: 5, size: 16 },\n  md: { radius: 7, size: 20 },\n} as const;\n\n// The web's 2.1 stroke on a 16-point grid, redrawn on GlassIcon's 20-point grid.\nconst MARK_STROKE = 2.1 * (20 / 16);\n\ntype MarkProps = {\n  name: GlassIconName;\n  progress: Animated.Value;\n  size: number;\n};\n\n// A mark grows in from half size as it fades in, and shrinks back as it fades out.\nfunction Mark({ name, progress, size }: MarkProps) {\n  const { colors } = useGlassTheme();\n\n  return (\n    <Animated.View\n      style={[\n        StyleSheet.absoluteFill,\n        styles.centre,\n        {\n          opacity: progress.interpolate({\n            extrapolate: 'clamp',\n            inputRange: [0, 1],\n            outputRange: [0, 1],\n          }),\n          transform: [\n            { scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.5, 1] }) },\n          ],\n        },\n      ]}\n    >\n      <GlassIcon\n        color={colors.accentForeground}\n        name={name}\n        size={size * 0.84}\n        strokeWidth={MARK_STROKE}\n      />\n    </Animated.View>\n  );\n}\n\n/**\n * A checkbox for one choice or, with `checked=\"indeterminate\"`, for a parent of several. The box\n * fills with the accent and its mark draws in; unchecking runs the same way back.\n */\nexport const GlassCheckbox = forwardRef<View, GlassCheckboxProps>(\n  (\n    {\n      checked,\n      defaultChecked = false,\n      description,\n      disabled,\n      invalid = false,\n      label,\n      onCheckedChange,\n      onPressIn,\n      onPressOut,\n      size = 'md',\n      style,\n      ...rest\n    },\n    ref,\n  ) => {\n    const { preferences } = useGlassTheme();\n    const [state, setState] = useControllableState<GlassCheckedState>(checked, defaultChecked);\n    const tick = useDrawIn(state === true, preferences.reduceMotion);\n    const dash = useDrawIn(state === 'indeterminate', preferences.reduceMotion);\n    const press = usePressDown(preferences.reduceMotion, onPressIn, onPressOut);\n    const box = BOX[size];\n\n    // A lone box still answers a 44-point touch.\n    const hitSlop = label === undefined ? (44 - box.size) / 2 : undefined;\n\n    // An indeterminate box becomes checked, as on the web.\n    const toggle = useCallback(() => {\n      const next = state !== true;\n\n      setState(next);\n      onCheckedChange?.(next);\n    }, [onCheckedChange, setState, state]);\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityHint={description}\n        accessibilityLabel={label}\n        accessibilityRole=\"checkbox\"\n        accessibilityState={{\n          checked: state === 'indeterminate' ? 'mixed' : state,\n          disabled: Boolean(disabled),\n        }}\n        disabled={disabled}\n        hitSlop={hitSlop}\n        onPress={toggle}\n        onPressIn={press.handlePressIn}\n        onPressOut={press.handlePressOut}\n        style={[\n          label === undefined ? checkFieldStyles.alone : checkFieldStyles.field,\n          label !== undefined && size === 'sm' && checkFieldStyles.fieldSmall,\n          disabled && checkFieldStyles.disabled,\n          style,\n        ]}\n        {...rest}\n      >\n        <CheckWell\n          chosen={state !== false}\n          invalid={invalid}\n          press={press.press}\n          radius={box.radius}\n          size={box.size}\n        >\n          <Mark name=\"check\" progress={tick} size={box.size} />\n          <Mark name=\"dash\" progress={dash} size={box.size} />\n        </CheckWell>\n        {label === undefined ? null : (\n          <CheckText description={description} label={label} size={size} />\n        )}\n      </Pressable>\n    );\n  },\n);\n\nGlassCheckbox.displayName = 'GlassCheckbox';\n\nconst styles = StyleSheet.create({\n  centre: { alignItems: 'center', justifyContent: 'center' },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-checkbox.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"
    },
    {
      "path": "registry/glassyui-native/lib/icons.tsx",
      "content": "import { View } from 'react-native';\nimport Svg, { Path } from 'react-native-svg';\n\nexport type GlassIconName =\n  | 'check'\n  | 'chevron-down'\n  | 'chevron-right'\n  | 'close'\n  | 'dash'\n  | 'danger'\n  | 'info'\n  | 'success'\n  | 'warning';\n\n// The web components' own strokes, drawn on a 20-point grid.\nconst PATHS = {\n  check: ['M4.5 10.5 8.25 14.25 15.5 6.25'],\n  'chevron-down': ['M5 7.5l5 5 5-5'],\n  'chevron-right': ['M7.5 4.4 13.1 10l-5.6 5.6'],\n  close: ['M5.5 5.5l9 9m0-9l-9 9'],\n  dash: ['M5 10h10'],\n  danger: ['M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z', 'M7.6 7.6l4.8 4.8', 'M12.4 7.6l-4.8 4.8'],\n  info: ['M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z', 'M10 9.2v4.1', 'M10 6.7h.01'],\n  success: ['M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14Z', 'M6.9 10.2l2.2 2.2 4-4.4'],\n  warning: ['M10 3.6 17 16H3L10 3.6Z', 'M10 8.4v3.4', 'M10 14h.01'],\n} as const satisfies Record<GlassIconName, readonly string[]>;\n\nexport type GlassIconProps = {\n  color: string;\n  name: GlassIconName;\n  size?: number;\n  strokeWidth?: number;\n};\n\n/** A stroked glyph from the library's small icon set. Decorative; label its control instead. */\nexport function GlassIcon({ color, name, size = 20, strokeWidth = 1.8 }: GlassIconProps) {\n  // The hiding lives on a View: on the web, Svg passes its props to a bare <svg> element.\n  return (\n    <View\n      accessibilityElementsHidden\n      importantForAccessibility=\"no-hide-descendants\"\n      style={{ height: size, width: size }}\n    >\n      <Svg width={size} height={size} viewBox=\"0 0 20 20\" fill=\"none\">\n        {PATHS[name].map((path) => (\n          <Path\n            key={path}\n            d={path}\n            stroke={color}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth={strokeWidth}\n          />\n        ))}\n      </Svg>\n    </View>\n  );\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/icons.tsx"
    }
  ],
  "type": "registry:ui"
}