{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glass-accordion",
  "title": "Glass Accordion",
  "description": "Sections that expand and collapse on glass, one at a time or several.",
  "dependencies": [
    "react-native-svg"
  ],
  "registryDependencies": [
    "@glassyui-native/glass-panel",
    "@glassyui-native/glass-provider",
    "@glassyui-native/glass-surface"
  ],
  "files": [
    {
      "path": "registry/glassyui-native/glass-accordion.tsx",
      "content": "import {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n  type ReactNode,\n} from 'react';\nimport {\n  Animated,\n  Easing,\n  Pressable,\n  StyleSheet,\n  Text,\n  View,\n  type GestureResponderEvent,\n  type LayoutChangeEvent,\n  type PressableProps,\n  type StyleProp,\n  type ViewProps,\n  type ViewStyle,\n} from 'react-native';\nimport { GlassPanel } from './glass-panel';\nimport { useGlassTheme } from './glass-provider';\nimport { GlassSurface } from './glass-surface';\nimport { GlassIcon } from './lib/icons';\nimport { wrapPlainText } from './lib/plain-text';\nimport { GLASS_MOTION, GLASS_RADIUS } from './lib/tokens';\nimport { useControllableState } from './lib/use-controllable-state';\n\ntype SingleAccordionProps = {\n  /** One item open at a time. The default. */\n  type?: 'single';\n  /** The open item's value, or an empty string for none. */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Lets the open item close again, leaving none open. */\n  collapsible?: boolean;\n};\n\ntype MultipleAccordionProps = {\n  /** Any number of items open at once. */\n  type: 'multiple';\n  /** The open items' values. */\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  /** Every item of a multiple accordion can close. */\n  collapsible?: never;\n};\n\nexport type GlassAccordionProps = ViewProps &\n  (SingleAccordionProps | MultipleAccordionProps) & {\n    /** Turns off every item. */\n    disabled?: boolean;\n  };\n\ntype AccordionState = {\n  /** Whether an open item can close. */\n  collapsible: boolean;\n  disabled: boolean;\n  open: readonly string[];\n  toggle: (item: string) => void;\n};\n\ntype ItemState = {\n  disabled: boolean;\n  open: boolean;\n  value: string;\n};\n\nconst AccordionContext = createContext<AccordionState | null>(null);\n\nconst ItemContext = createContext<ItemState | null>(null);\n\n// Arrivals ease out on the web's curve; exits ease in and run shorter.\nconst EASE_OUT = Easing.bezier(0.32, 0.72, 0, 1);\n\nconst EASE_IN = Easing.bezier(0.42, 0, 1, 1);\n\n// CSS's own ease, which the web chevron turns on.\nconst EASE = Easing.bezier(0.25, 0.1, 0.25, 1);\n\nfunction useAccordion(component: string) {\n  const accordion = useContext(AccordionContext);\n\n  if (!accordion) throw new Error(`${component} must be used within GlassAccordion`);\n\n  return accordion;\n}\n\nfunction useItem(component: string) {\n  const item = useContext(ItemContext);\n\n  if (!item) throw new Error(`${component} must be used within GlassAccordionItem`);\n\n  return item;\n}\n\n/** A stack of items that open to show more, one at a time or several at once. */\nexport const GlassAccordion = forwardRef<View, GlassAccordionProps>(\n  (\n    { collapsible = false, defaultValue, disabled = false, onValueChange, type, value, ...rest },\n    ref,\n  ) => {\n    // Both kinds keep their own state; only the one `type` names is used.\n    const [single, setSingle] = useControllableState(\n      type === 'multiple' ? undefined : value,\n      type === 'multiple' ? '' : (defaultValue ?? ''),\n      type === 'multiple' ? undefined : onValueChange,\n    );\n\n    const [many, setMany] = useControllableState(\n      type === 'multiple' ? value : undefined,\n      type === 'multiple' ? (defaultValue ?? []) : [],\n      type === 'multiple' ? onValueChange : undefined,\n    );\n\n    const toggle = useCallback(\n      (item: string) => {\n        if (type === 'multiple') {\n          setMany(many.includes(item) ? many.filter((entry) => entry !== item) : [...many, item]);\n\n          return;\n        }\n\n        if (item !== single) setSingle(item);\n        else if (collapsible) setSingle('');\n      },\n      [collapsible, many, setMany, setSingle, single, type],\n    );\n\n    const open = useMemo(\n      () => (type === 'multiple' ? many : single ? [single] : []),\n      [many, single, type],\n    );\n\n    const state = useMemo(\n      () => ({ collapsible: type === 'multiple' || collapsible, disabled, open, toggle }),\n      [collapsible, disabled, open, toggle, type],\n    );\n\n    return (\n      <AccordionContext.Provider value={state}>\n        <View ref={ref} {...rest} />\n      </AccordionContext.Provider>\n    );\n  },\n);\n\nGlassAccordion.displayName = 'GlassAccordion';\n\nexport type GlassAccordionItemProps = ViewProps & {\n  disabled?: boolean;\n  /** Names the item. The accordion's value holds the names of the open items. */\n  value: string;\n};\n\n/** One item of an accordion: a trigger and the content it opens. */\nexport const GlassAccordionItem = forwardRef<View, GlassAccordionItemProps>(\n  ({ disabled = false, value, ...rest }, ref) => {\n    const accordion = useAccordion('GlassAccordionItem');\n    const open = accordion.open.includes(value);\n    const off = accordion.disabled || disabled;\n    const item = useMemo(() => ({ disabled: off, open, value }), [off, open, value]);\n\n    return (\n      <ItemContext.Provider value={item}>\n        <View ref={ref} {...rest} />\n      </ItemContext.Provider>\n    );\n  },\n);\n\nGlassAccordionItem.displayName = 'GlassAccordionItem';\n\nexport type GlassAccordionTriggerProps = Omit<PressableProps, 'children' | 'style'> & {\n  children?: ReactNode;\n  style?: StyleProp<ViewStyle>;\n};\n\n/** The glass row that opens and closes its item; its chevron turns to show which. */\nexport const GlassAccordionTrigger = forwardRef<View, GlassAccordionTriggerProps>(\n  ({ children, disabled, onPress, onPressIn, onPressOut, style, ...rest }, ref) => {\n    const { colors, preferences } = useGlassTheme();\n    const accordion = useAccordion('GlassAccordionTrigger');\n    const item = useItem('GlassAccordionTrigger');\n    const [turn] = useState(() => new Animated.Value(item.open ? 1 : 0));\n    const [press] = useState(() => new Animated.Value(0));\n    const off = Boolean(disabled) || item.disabled;\n    const reduceMotion = preferences.reduceMotion;\n\n    useEffect(() => {\n      if (reduceMotion) {\n        turn.setValue(item.open ? 1 : 0);\n\n        return;\n      }\n\n      const animation = Animated.timing(turn, {\n        duration: GLASS_MOTION.duration,\n        easing: EASE,\n        toValue: item.open ? 1 : 0,\n        useNativeDriver: GLASS_MOTION.nativeDriver,\n      });\n\n      animation.start();\n\n      return () => animation.stop();\n    }, [item.open, reduceMotion, turn]);\n\n    const handlePress = useCallback(\n      (event: GestureResponderEvent) => {\n        onPress?.(event);\n        accordion.toggle(item.value);\n      },\n      [accordion, item.value, onPress],\n    );\n\n    // A finger lights the glass, as it lights a button.\n    const handlePressIn = useCallback(\n      (event: GestureResponderEvent) => {\n        onPressIn?.(event);\n\n        if (reduceMotion) {\n          press.setValue(1);\n\n          return;\n        }\n\n        Animated.timing(press, {\n          duration: 90,\n          easing: Easing.out(Easing.quad),\n          toValue: 1,\n          useNativeDriver: GLASS_MOTION.nativeDriver,\n        }).start();\n      },\n      [onPressIn, press, reduceMotion],\n    );\n\n    const handlePressOut = useCallback(\n      (event: GestureResponderEvent) => {\n        onPressOut?.(event);\n\n        if (reduceMotion) {\n          press.setValue(0);\n\n          return;\n        }\n\n        Animated.timing(press, {\n          duration: GLASS_MOTION.duration,\n          easing: EASE,\n          toValue: 0,\n          useNativeDriver: GLASS_MOTION.nativeDriver,\n        }).start();\n      },\n      [onPressOut, press, reduceMotion],\n    );\n\n    return (\n      <Pressable\n        ref={ref}\n        accessibilityRole=\"button\"\n        // An open item that cannot close announces itself as unavailable, as on the web.\n        accessibilityState={{\n          disabled: off || (item.open && !accordion.collapsible),\n          expanded: item.open,\n        }}\n        disabled={off}\n        onPress={handlePress}\n        onPressIn={handlePressIn}\n        onPressOut={handlePressOut}\n        style={[off && styles.disabled, style]}\n        {...rest}\n      >\n        <GlassSurface elevation=\"none\" preset=\"control\" style={styles.trigger}>\n          <View style={[StyleSheet.absoluteFill, styles.clip]}>\n            <Animated.View\n              style={[StyleSheet.absoluteFill, { backgroundColor: colors.rim, opacity: press }]}\n            />\n          </View>\n          <View style={styles.label}>\n            {wrapPlainText(children, (text) => (\n              <Text style={[styles.labelText, { color: colors.foreground }]}>{text}</Text>\n            ))}\n          </View>\n          <Animated.View\n            style={{\n              transform: [\n                {\n                  rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }),\n                },\n              ],\n            }}\n          >\n            <GlassIcon\n              color={colors.mutedForeground}\n              name=\"chevron-down\"\n              size={16}\n              strokeWidth={1.6}\n            />\n          </Animated.View>\n        </GlassSurface>\n      </Pressable>\n    );\n  },\n);\n\nGlassAccordionTrigger.displayName = 'GlassAccordionTrigger';\n\nexport type GlassAccordionContentProps = ViewProps;\n\n/**\n * The glass card an item opens, unrolling like a curtain; Reduce Motion opens and closes it at\n * once. `style` lays out what sits inside the card.\n */\nexport const GlassAccordionContent = forwardRef<View, GlassAccordionContentProps>(\n  ({ children, onLayout, style, ...rest }, ref) => {\n    const { colors, preferences } = useGlassTheme();\n    const item = useItem('GlassAccordionContent');\n    const [height, setHeight] = useState(0);\n    const [unfold] = useState(() => new Animated.Value(item.open ? 1 : 0));\n\n    useEffect(() => {\n      if (preferences.reduceMotion) {\n        unfold.setValue(item.open ? 1 : 0);\n\n        return;\n      }\n\n      const animation = Animated.timing(unfold, {\n        duration: item.open ? 300 : 220,\n        easing: item.open ? EASE_OUT : EASE_IN,\n        toValue: item.open ? 1 : 0,\n        // Height is layout, which only the JavaScript driver can animate.\n        useNativeDriver: false,\n      });\n\n      animation.start();\n\n      return () => animation.stop();\n    }, [item.open, preferences.reduceMotion, unfold]);\n\n    // The content keeps its full height inside the folded card, so it can be measured shut.\n    const measure = useCallback(\n      (event: LayoutChangeEvent) => {\n        onLayout?.(event);\n        setHeight(event.nativeEvent.layout.height);\n      },\n      [onLayout],\n    );\n\n    return (\n      <Animated.View\n        accessibilityElementsHidden={!item.open}\n        importantForAccessibility={item.open ? 'auto' : 'no-hide-descendants'}\n        style={{\n          // Until the content is measured, an open item takes its natural height.\n          height:\n            height > 0\n              ? unfold.interpolate({ inputRange: [0, 1], outputRange: [0, height] })\n              : item.open\n                ? undefined\n                : 0,\n          marginTop: unfold.interpolate({ inputRange: [0, 1], outputRange: [0, 8] }),\n          pointerEvents: item.open ? 'auto' : 'none',\n        }}\n      >\n        {/* The card clips what it holds, not its own shadow, and shrinks with the row. */}\n        <GlassPanel style={styles.card} variant=\"card\">\n          <View ref={ref} onLayout={measure} style={[styles.content, style]} {...rest}>\n            {wrapPlainText(children, (text) => (\n              <Text style={[styles.contentText, { color: colors.foreground }]}>{text}</Text>\n            ))}\n          </View>\n        </GlassPanel>\n      </Animated.View>\n    );\n  },\n);\n\nGlassAccordionContent.displayName = 'GlassAccordionContent';\n\nconst styles = StyleSheet.create({\n  card: { flexGrow: 1, flexShrink: 1, overflow: 'hidden', padding: 0 },\n  clip: {\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.md,\n    overflow: 'hidden',\n    pointerEvents: 'none',\n  },\n  content: { flexShrink: 0, padding: 20 },\n  contentText: { fontSize: 14, lineHeight: 20 },\n  disabled: { opacity: 0.45 },\n  label: { flex: 1 },\n  labelText: { fontSize: 16, fontWeight: '500', lineHeight: 24 },\n  // The trigger shares the content card's corner radius, as on the web.\n  trigger: {\n    alignItems: 'center',\n    borderCurve: 'continuous',\n    borderRadius: GLASS_RADIUS.md,\n    flexDirection: 'row',\n    gap: 12,\n    justifyContent: 'space-between',\n    paddingHorizontal: 16,\n    paddingVertical: 12,\n  },\n});\n",
      "type": "registry:ui",
      "target": "components/glassyui/glass-accordion.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"
    },
    {
      "path": "registry/glassyui-native/lib/plain-text.ts",
      "content": "import { Children, isValidElement, type ReactNode } from 'react';\n\n/**\n * Sets plain text inside a Text, which React Native needs to render it. Content that is all text\n * becomes one Text; beside elements such as icons, each run of text gets its own.\n */\nexport function wrapPlainText(node: ReactNode, render: (text: ReactNode) => ReactNode) {\n  const parts = Children.toArray(node);\n\n  if (parts.length === 0) return null;\n\n  if (parts.every((part) => !isValidElement(part))) return render(node);\n\n  return Children.map(node, (part) => (isValidElement(part) ? part : render(part)));\n}\n\n/** The words in `node` when it is only text, to name a control for assistive technology. */\nexport function plainTextOf(node: ReactNode) {\n  const parts = Children.toArray(node);\n\n  return parts.length > 0 && parts.every((part) => !isValidElement(part))\n    ? parts.join('')\n    : undefined;\n}\n",
      "type": "registry:lib",
      "target": "components/glassyui/lib/plain-text.ts"
    }
  ],
  "type": "registry:ui"
}