{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radio-group-glass",
  "title": "Group-glass radio",
  "description": "Styled radio: the Group-glass set. 5 decorative radio variations (FrostRadio, TintRadio, SmokeRadio, CrystalRadio, DepthRadio) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens",
    "@ai2/glass"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/radio-group-glass.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { glassDepth } from \"@/registry/ai2/ui/glass\"\n\n/* Glass radio-group family: 5 frosted-glass selection groups. The glass is NO\n   LONGER hand-written: the container surface derives from the glassDepth scale\n   inside @ai2/glass, so the ai2 signature (top inset highlight + ambient shadow)\n   speaks the same language in every variant.\n\n   Choosing the depth (a bar-shaped control): a segmented group is wide but SHORT\n   and sits in the page flow; behind a short bar a heavy blur brings no gain, so the\n   scale starts low and only the genuinely floating variant reaches the top.\n\n   NO GLASS ON GLASS (it used to be that way, now fixed): the container and the\n   sliding marker each used to carry their own backdrop-blur - with two layers of\n   glass on top of each other the image turned to mud. The glass is now ONLY on the\n   bar; the selected marker is an OPAQUE highlight on top of it (with no blur of its\n   own). Since the marker does not carry glassDepth, using shadow-* on it is safe.\n\n   role=\"radiogroup\" + role=\"radio\", roving tabindex, arrow-key navigation and\n   selection. Color comes ONLY from tokens, via alpha color-mix. The layoutId is\n   unique per instance via useId. Under reduced-motion the marker jumps instantly.\n   Renders with no props too. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype RadioOption = { value: string; label: React.ReactNode }\n\ninterface RadioGroupProps {\n  className?: string\n  size?: StyledSize\n  options?: RadioOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst DEFAULT_OPTIONS: RadioOption[] = [\n  { value: \"a\", label: \"Option A\" },\n  { value: \"b\", label: \"Option B\" },\n  { value: \"c\", label: \"Option C\" },\n]\n\nconst textSize: Record<StyledSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-base\",\n  xl: \"text-lg\",\n}\n\nconst padSize: Record<StyledSize, string> = {\n  sm: \"px-2.5 py-1\",\n  md: \"px-3 py-1.5\",\n  lg: \"px-4 py-2\",\n  xl: \"px-5 py-2.5\",\n}\n\nfunction useRadioGroup(props: Pick<RadioGroupProps, \"value\" | \"defaultValue\" | \"onValueChange\" | \"options\">) {\n  const { value, defaultValue, onValueChange, options } = props\n  const opts = options && options.length > 0 ? options : DEFAULT_OPTIONS\n  const reduce = useReducedMotion()\n  const [internal, setInternal] = React.useState(defaultValue ?? opts[0]?.value)\n  const selected = value ?? internal\n  const select = React.useCallback(\n    (v: string) => {\n      if (value === undefined) setInternal(v)\n      onValueChange?.(v)\n    },\n    [value, onValueChange]\n  )\n  const spring = reduce\n    ? { duration: 0 }\n    : ({ type: \"spring\" as const, stiffness: 500, damping: 34 })\n  return { opts, selected, select, reduce, spring }\n}\n\n/* Arrow-key navigation + selection (together with the roving tabindex). */\nfunction useRadioKeys(opts: RadioOption[], select: (v: string) => void) {\n  const refs = React.useRef<(HTMLButtonElement | null)[]>([])\n  const setRef = (i: number) => (el: HTMLButtonElement | null) => {\n    refs.current[i] = el\n  }\n  const onKeyDown = (i: number) => (e: React.KeyboardEvent) => {\n    const last = opts.length - 1\n    let next = -1\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") next = i === last ? 0 : i + 1\n    else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") next = i === 0 ? last : i - 1\n    if (next < 0) return\n    e.preventDefault()\n    const target = opts[next]\n    if (!target) return\n    select(target.value)\n    refs.current[next]?.focus()\n  }\n  return { setRef, onKeyDown }\n}\n\nconst focusRing =\n  \"outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n\n/* Shared shell: a frosted container plus a sliding frosted indicator. The variant difference lives only in surfaceClass / indicatorClass. */\nfunction GlassShell({\n  props,\n  surfaceClass,\n  indicatorClass,\n  activeText = \"text-foreground\",\n}: {\n  props: RadioGroupProps\n  surfaceClass: string\n  indicatorClass: string\n  activeText?: string\n}) {\n  const { className, size = \"md\", options, value, defaultValue, onValueChange } = props\n  const { opts, selected, select, spring } = useRadioGroup({ options, value, defaultValue, onValueChange })\n  const { setRef, onKeyDown } = useRadioKeys(opts, select)\n  const groupId = React.useId()\n  const activeIndex = opts.findIndex((o) => o.value === selected)\n\n  return (\n    <div\n      data-slot=\"styled-radio-group\"\n      role=\"radiogroup\"\n      aria-label=\"Choose an option\"\n      className={cn(\"inline-flex items-center gap-1 rounded-xl p-1\", surfaceClass, className)}\n    >\n      {opts.map((opt, i) => {\n        const active = opt.value === selected\n        const roving = active || (activeIndex < 0 && i === 0)\n        return (\n          <button\n            key={opt.value}\n            ref={setRef(i)}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={active}\n            tabIndex={roving ? 0 : -1}\n            onKeyDown={onKeyDown(i)}\n            onClick={() => select(opt.value)}\n            data-slot=\"styled-radio-item\"\n            className={cn(\n              \"relative z-10 cursor-pointer rounded-lg font-medium transition-colors\",\n              focusRing,\n              textSize[size],\n              padSize[size],\n              active ? activeText : \"text-muted-foreground hover:text-foreground\"\n            )}\n          >\n            {active ? (\n              <motion.span\n                layoutId={`glass-radio-${groupId}`}\n                transition={spring}\n                className={cn(\"absolute inset-0 -z-10 rounded-lg\", indicatorClass)}\n              />\n            ) : null}\n            <span className=\"relative\">{opt.label}</span>\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* Frost: a lightly frosted surface. DEPTH sm (4px): the plainest group; behind a short segmented bar anything more is an invisible cost. The marker: an opaque background chip (NO blur - the glass lives only in the bar). */\nexport function FrostRadio(props: RadioGroupProps) {\n  return (\n    <GlassShell\n      props={props}\n      surfaceClass={cn(\n        glassDepth.sm,\n        \"border border-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)]\"\n      )}\n      indicatorClass=\"border border-[color-mix(in_oklab,var(--color-foreground)_12%,transparent)] bg-background shadow-sm\"\n    />\n  )\n}\n\n/* Tint: an info-toned frosted surface. DEPTH md (8px, the canonical default): the character comes from the TONE, not the blur. Tint overrides both the bg-* and the supports-* step. The marker: an OPAQUE mix of info and background; text-primary (the semantic role of the active state) reads clearly on top of it. */\nexport function TintRadio(props: RadioGroupProps) {\n  return (\n    <GlassShell\n      props={props}\n      activeText=\"text-primary\"\n      surfaceClass={cn(\n        glassDepth.md,\n        \"border border-[color-mix(in_oklab,var(--color-info)_18%,transparent)]\",\n        \"bg-[color-mix(in_oklab,var(--color-info)_8%,transparent)]\",\n        \"supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-info)_8%,transparent)]\"\n      )}\n      indicatorClass=\"border border-[color-mix(in_oklab,var(--color-info)_30%,transparent)] bg-[color-mix(in_oklab,var(--color-info)_16%,var(--color-background))] shadow-sm\"\n    />\n  )\n}\n\n/* Smoke: a smoky surface. DEPTH md (8px): DELIBERATELY the same step as Tint - glass at the same distance, the difference being tone only (foreground smoke versus info). The marker: a bright opaque chip cutting through the smoke. */\nexport function SmokeRadio(props: RadioGroupProps) {\n  return (\n    <GlassShell\n      props={props}\n      surfaceClass={cn(\n        glassDepth.md,\n        \"border border-[color-mix(in_oklab,var(--color-foreground)_14%,transparent)]\",\n        \"bg-[color-mix(in_oklab,var(--color-foreground)_16%,transparent)]\",\n        \"supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-foreground)_16%,transparent)]\"\n      )}\n      indicatorClass=\"bg-background shadow-sm\"\n    />\n  )\n}\n\n/* Crystal: a clear pane. DEPTH lg (16px): diffusion is the SUBJECT here, not decoration. The gradient removes the bg-* step but does NOT remove the supports-* step, which is why it is explicitly pulled to bg-transparent. The marker: an opaque chip plus a bright top edge. */\nexport function CrystalRadio(props: RadioGroupProps) {\n  return (\n    <GlassShell\n      props={props}\n      surfaceClass={cn(\n        glassDepth.lg,\n        \"border border-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)]\",\n        \"bg-gradient-to-b supports-[backdrop-filter]:bg-transparent\",\n        \"from-[color-mix(in_oklab,var(--color-background)_50%,transparent)]\",\n        \"to-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)]\",\n        \"supports-[backdrop-filter]:backdrop-saturate-150\"\n      )}\n      indicatorClass=\"border border-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)] border-t-[color-mix(in_oklab,var(--color-background)_85%,transparent)] bg-background shadow-sm\"\n    />\n  )\n}\n\n/* Depth: layered depth. DEPTH xl (24px): the ONLY variant that earns the top of the scale - the bar reads like a recessed bed with the indicator floating over it; the glass furthest from the surface, with the most scattered background. Note: the `shadow-inner` on the shell was removed - it would override the same CSS property as the glassDepth signature. The sense of depth now comes from the floating indicator's shadow-lg (the indicator carries no glassDepth, so shadow-* is safe there). */\nexport function DepthRadio(props: RadioGroupProps) {\n  return (\n    <GlassShell\n      props={props}\n      surfaceClass={cn(\n        glassDepth.xl,\n        \"border border-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)]\",\n        \"bg-[color-mix(in_oklab,var(--color-foreground)_7%,transparent)]\",\n        \"supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-foreground)_7%,transparent)]\"\n      )}\n      indicatorClass=\"border border-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)] bg-background shadow-lg\"\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/radio-group-glass.tsx"
    }
  ],
  "categories": [
    "styled",
    "radio"
  ],
  "type": "registry:component"
}