{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-segmented-control",
  "title": "Glass Segmented Control",
  "description": "A single-choice control whose glass bubble springs between options.",
  "registryDependencies": [
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-segmented-control.tsx",
      "content": "import {\n  Children,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  type ReactNode,\n} from 'react';\nimport {\n  Animated,\n  Pressable,\n  StyleSheet,\n  Text,\n  View,\n  type LayoutChangeEvent,\n  type LayoutRectangle,\n  type PressableProps,\n  type StyleProp,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { GLASS_RADIUS } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\nimport { useSelectionIndicator } from './lib/use-selection-indicator';\n\ntype SegmentedContextValue = {\n  disabled: boolean;\n  forgetItem: (value: string) => void;\n  onItemLayout: (value: string, layout: LayoutRectangle) => void;\n  select: (value: string) => void;\n  selected: string;\n};\n\nconst SegmentedContext = createContext<SegmentedContextValue | null>(null);\n\nfunction useSegmentedControl() {\n  const context = useContext(SegmentedContext);\n\n  if (!context) {\n    throw new Error('GlassSegmentedControlItem must be used within GlassSegmentedControl');\n  }\n\n  return context;\n}\n\n// The raised bubble under the chosen option, from .lg-segmented-indicator.\nconst BUBBLE = {\n  dark: {\n    backgroundColor: 'rgba(255, 255, 255, 0.14)',\n    boxShadow:\n      'inset 0px 1px 0px rgba(255, 255, 255, 0.22), inset 0px -1px 0px rgba(0, 0, 0, 0.12), 0px 1px 3px rgba(0, 0, 0, 0.24), 0px 4px 12px -4px rgba(0, 0, 0, 0.4)',\n  },\n  light: {\n    backgroundColor: 'rgba(255, 255, 255, 0.9)',\n    boxShadow:\n      'inset 0px 1px 0px rgba(255, 255, 255, 0.9), 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 4px 12px -4px rgba(0, 0, 0, 0.16)',\n  },\n} as const;\n\n// Light glass over a pale backdrop would hide the white bubble, so the track sinks into a faint\n// slate well.\nconst LIGHT_WELL = 'rgba(100, 108, 140, 0.12)';\n\nconst INSET = 3;\n\nexport type GlassSegmentedControlProps = ViewProps & {\n  defaultValue?: string;\n  /** Turns every option off. */\n  disabled?: boolean;\n  onValueChange?: (value: string) => void;\n  value?: string;\n};\n\n/**\n * A single choice from two to six options, in a glass capsule. A raised bubble springs between\n * the options; Reduce Motion moves it at once.\n */\nexport const GlassSegmentedControl = forwardRef<View, GlassSegmentedControlProps>(\n  (\n    { children, defaultValue, disabled = false, onLayout, onValueChange, style, value, ...rest },\n    ref,\n  ) => {\n    const { preferences } = useGlassTheme();\n    const [selected, select] = useControllableState(value, defaultValue ?? '', onValueChange);\n\n    const { forgetItem, onItemLayout, onTrackLayout, placement } = useSelectionIndicator(\n      selected,\n      preferences.reduceMotion,\n    );\n\n    const handleLayout = useCallback(\n      (event: LayoutChangeEvent) => {\n        onTrackLayout(event);\n        onLayout?.(event);\n      },\n      [onLayout, onTrackLayout],\n    );\n\n    const context = useMemo(\n      () => ({ disabled, forgetItem, onItemLayout, select, selected }),\n      [disabled, forgetItem, onItemLayout, select, selected],\n    );\n\n    return (\n      <SegmentedContext.Provider value={context}>\n        <GlassSurface\n          ref={ref}\n          accessibilityRole=\"radiogroup\"\n          accessibilityState={{ disabled }}\n          elevation=\"base\"\n          onLayout={handleLayout}\n          preset=\"control\"\n          style={[styles.track, style]}\n          {...rest}\n        >\n          {preferences.theme === 'light' && !preferences.reduceTransparency ? (\n            <View\n              style={[\n                { pointerEvents: 'none' },\n                StyleSheet.absoluteFill,\n                styles.round,\n                { backgroundColor: LIGHT_WELL },\n              ]}\n            />\n          ) : null}\n          {placement ? (\n            <Animated.View\n              style={[\n                { pointerEvents: 'none' },\n                styles.indicator,\n                BUBBLE[preferences.theme],\n                {\n                  height: placement.height,\n                  top: placement.top,\n                  width: placement.width,\n                  transform: [{ translateX: placement.offset }],\n                },\n              ]}\n            />\n          ) : null}\n          {children}\n        </GlassSurface>\n      </SegmentedContext.Provider>\n    );\n  },\n);\n\nGlassSegmentedControl.displayName = 'GlassSegmentedControl';\n\nexport type GlassSegmentedControlItemProps = Omit<\n  PressableProps,\n  'children' | 'onPress' | 'style'\n> & {\n  children?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n  value: string;\n};\n\n/** One option in a GlassSegmentedControl. Plain text children are set as its label. */\nexport const GlassSegmentedControlItem = forwardRef<View, GlassSegmentedControlItemProps>(\n  ({ children, disabled, onLayout, style, value, ...rest }, ref) => {\n    const { colors } = useGlassTheme();\n    const control = useSegmentedControl();\n    const layout = useRef<LayoutRectangle | null>(null);\n    const { forgetItem, onItemLayout, select } = control;\n    const checked = control.selected === value;\n    const off = control.disabled || Boolean(disabled);\n\n    // Re-registers under a new value, and leaves the indicator nothing to find once unmounted.\n    useEffect(() => {\n      if (layout.current) onItemLayout(value, layout.current);\n\n      return () => forgetItem(value);\n    }, [forgetItem, onItemLayout, value]);\n\n    const handleLayout = useCallback(\n      (event: LayoutChangeEvent) => {\n        layout.current = event.nativeEvent.layout;\n        onItemLayout(value, event.nativeEvent.layout);\n        onLayout?.(event);\n      },\n      [onItemLayout, onLayout, value],\n    );\n\n    const choose = useCallback(() => {\n      if (!checked) select(value);\n    }, [checked, select, value]);\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"radio\"\n        accessibilityState={{ checked, disabled: off }}\n        disabled={off}\n        onLayout={handleLayout}\n        onPress={choose}\n        style={[styles.item, off && styles.disabled, style]}\n        {...rest}\n      >\n        {Children.toArray(children).every((child) => !isValidElement(child)) ? (\n          <Text\n            numberOfLines={1}\n            style={[styles.label, { color: checked ? colors.foreground : colors.mutedForeground }]}\n          >\n            {children}\n          </Text>\n        ) : (\n          children\n        )}\n      </Pressable>\n    );\n  },\n);\n\nGlassSegmentedControlItem.displayName = 'GlassSegmentedControlItem';\n\nconst styles = StyleSheet.create({\n  disabled: { opacity: 0.45 },\n  indicator: {\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    position: 'absolute',\n    start: 0,\n  },\n  // The web's 34-point option, raised to 38 so the 44-point track is the touch target.\n  item: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.capsule,\n    flex: 1,\n    flexDirection: 'row',\n    gap: 6,\n    justifyContent: 'center',\n    minHeight: 38,\n    minWidth: 0,\n    paddingHorizontal: 14,\n    paddingVertical: 7,\n  },\n  label: { fontSize: 14, fontWeight: '500', lineHeight: 20 },\n  round: { borderCurve: 'continuous', borderRadius: GLASS_RADIUS.capsule },\n  track: {\n    alignSelf: 'stretch',\n    borderRadius: GLASS_RADIUS.capsule,\n    flexDirection: 'row',\n    padding: INSET,\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-segmented-control.tsx"
    },
    {
      "path": "registry/glassyui-native/lib/use-selection-indicator.ts",
      "content": "import { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport { Animated, I18nManager, type LayoutChangeEvent, type LayoutRectangle } from 'react-native';\nimport { GLASS_MOTION } from './tokens';\n\n/** Where the indicator sits: the selected item's box, and the offset that slides it there. */\nexport type IndicatorPlacement = {\n  height: number;\n  offset: Animated.Value;\n  top: number;\n  width: number;\n};\n\nfunction sameBox(previous: LayoutRectangle | undefined, next: LayoutRectangle) {\n  return (\n    previous !== undefined &&\n    previous.x === next.x &&\n    previous.y === next.y &&\n    previous.width === next.width &&\n    previous.height === next.height\n  );\n}\n\n/**\n * Slides one indicator between the items of a track, such as a segmented control's options or\n * a tab list's triggers. Items report their layout; the indicator sits at the track's start edge\n * and springs to the selected item. It jumps instead on its first placement, after a layout\n * change and under Reduce Motion.\n */\nexport function useSelectionIndicator(selected: string, reduceMotion: boolean) {\n  const [items, setItems] = useState<ReadonlyMap<string, LayoutRectangle>>(() => new Map());\n  const [trackWidth, setTrackWidth] = useState(0);\n  const [offset] = useState(() => new Animated.Value(0));\n  const placedOn = useRef<string | null>(null);\n  const box = items.get(selected);\n  const measured = box !== undefined;\n\n  // Layout reports physical positions. In RTL the start edge is the track's right edge, so the\n  // indicator travels left from there.\n  const start = measured ? (I18nManager.isRTL ? box.x + box.width - trackWidth : box.x) : 0;\n\n  useLayoutEffect(() => {\n    if (!measured) {\n      placedOn.current = null;\n\n      return;\n    }\n\n    // A new selection slides; the first placement and a relayout of the same item jump.\n    if (reduceMotion || placedOn.current === null || placedOn.current === selected) {\n      offset.setValue(start);\n    } else {\n      Animated.spring(offset, {\n        ...GLASS_MOTION.spring,\n        toValue: start,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      }).start();\n    }\n\n    placedOn.current = selected;\n  }, [measured, offset, reduceMotion, selected, start]);\n\n  const onTrackLayout = useCallback((event: LayoutChangeEvent) => {\n    setTrackWidth(event.nativeEvent.layout.width);\n  }, []);\n\n  const onItemLayout = useCallback((value: string, layout: LayoutRectangle) => {\n    setItems((previous) =>\n      sameBox(previous.get(value), layout) ? previous : new Map(previous).set(value, layout),\n    );\n  }, []);\n\n  const forgetItem = useCallback((value: string) => {\n    setItems((previous) => {\n      if (!previous.has(value)) return previous;\n\n      const next = new Map(previous);\n\n      next.delete(value);\n\n      return next;\n    });\n  }, []);\n\n  const placement: IndicatorPlacement | null = measured\n    ? { height: box.height, offset, top: box.y, width: box.width }\n    : null;\n\n  return { forgetItem, onItemLayout, onTrackLayout, placement };\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/use-selection-indicator.ts"
    }
  ],
  "type": "registry:ui"
}