{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "JellyRadio-JS-TW",
	"title": "JellyRadio",
	"description": "Radio group of labelled chips where the chosen one swells wide-then-tall on two springs and barges its neighbours outward with a travelling stagger, so a selection reads as a force moving through the row.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "JellyRadio/JellyRadio.jsx",
			"content": "'use client';\n\nimport { forwardRef, useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { animate, motion, motionValue, useReducedMotion, useTransform } from 'motion/react';\n\nconst DEFAULT_ITEMS = ['Off', 'Low', 'Medium', 'High', 'Max'];\nconst SIZES = { sm: [28, 12, 12], md: [36, 13, 16], lg: [44, 14, 20] };\n\nconst spring = (k, m, bounce) => ({\n  type: 'spring',\n  stiffness: k,\n  damping: 2 * Math.sqrt(k * m) * (1 - bounce),\n  mass: m\n});\n\nconst Chip = forwardRef(function Chip({ mv, children, ...rest }, ref) {\n  const transform = useTransform(() => `translateX(${mv.x.get()}px) scale(${mv.sx.get()}, ${mv.sy.get()})`);\n  return (\n    <motion.button ref={ref} style={{ transform }} {...rest}>\n      {children}\n    </motion.button>\n  );\n});\n\nexport default function JellyRadio({\n  items = DEFAULT_ITEMS,\n  value,\n  defaultValue,\n  onChange,\n  chipColor = '#27272a',\n  activeColor = '#f5f5f5',\n  textColor = '#f5f5f5',\n  activeTextColor = '#18181b',\n  size = 'md',\n  gap = 8,\n  radius = 18,\n  swell = 0.2,\n  barge = 6,\n  shrink = 0.05,\n  jelly = 1,\n  bounce = 0.25,\n  stagger = 22,\n  stiffness = 580,\n  disabled = false,\n  ariaLabel = 'Options',\n  className = ''\n}) {\n  const list = items.map(it => (typeof it === 'string' ? { value: it, label: it } : it));\n  const [inner, setInner] = useState(() => defaultValue ?? list[0]?.value);\n  const current = value ?? inner;\n  const at = Math.max(\n    0,\n    list.findIndex(it => it.value === current)\n  );\n  const reduce = useReducedMotion();\n  const groupRef = useRef(null);\n  const chipRefs = useRef([]);\n  const widths = useRef([]);\n  const mvs = useRef([]);\n  const applied = useRef(at);\n  const cfg = useRef({});\n  cfg.current = { swell, barge, shrink, jelly, bounce, stagger, stiffness, reduce, count: list.length };\n  const [h, font, px] = SIZES[size] ?? SIZES.md;\n  const itemsKey = list.map(it => it.value).join('|');\n\n  const mvFor = i => {\n    let mv = mvs.current[i];\n    if (!mv) {\n      mv = { x: motionValue(0), sx: motionValue(1), sy: motionValue(1) };\n      mvs.current[i] = mv;\n    }\n    return mv;\n  };\n\n  const apply = (sel, instant) => {\n    const C = cfg.current;\n    const group = groupRef.current;\n    const rtl = group ? getComputedStyle(group).direction === 'rtl' : false;\n    const push = ((widths.current[sel] ?? 0) * C.swell) / 2 + C.barge;\n    for (let i = 0; i < C.count; i++) {\n      const mv = mvFor(i);\n      const on = i === sel;\n      const far = Math.abs(i - sel);\n      const dir = Math.sign(i - sel) * (rtl ? -1 : 1);\n      const x = dir * push;\n      const s = on ? 1 + C.swell : 1 - C.shrink;\n      if (instant || C.reduce) {\n        mv.x.jump(x);\n        mv.sx.jump(s);\n        mv.sy.jump(s);\n        continue;\n      }\n      const k = C.stiffness * (1 - 0.12 * Math.min(far, 3));\n      const inFlight = mv.x.isAnimating() || mv.sx.isAnimating() || mv.sy.isAnimating();\n      const delay = inFlight ? 0 : (far * C.stagger) / 1000;\n      animate(mv.x, x, { ...spring(k, 0.9, C.bounce), delay });\n      const j = C.jelly;\n      animate(mv.sx, s, {\n        ...spring(k * (1 + 0.24 * j), 0.9 - 0.1 * j, Math.min(0.85, C.bounce + 0.3 * j)),\n        delay\n      });\n      animate(mv.sy, s, { ...spring(k * (1 - 0.14 * j), 0.9 + 0.05 * j, C.bounce), delay: delay + 0.05 * j });\n    }\n  };\n\n  const measure = () => {\n    const group = groupRef.current;\n    if (!group) return;\n    widths.current = chipRefs.current.map(el => el?.offsetWidth ?? 0);\n    const chipH = chipRefs.current[0]?.offsetHeight ?? 0;\n    const maxW = Math.max(0, ...widths.current);\n    group.style.setProperty('--jr-pad-x', `${Math.ceil((maxW * swell * 1.3) / 2 + barge) + 2}px`);\n    group.style.setProperty('--jr-pad-y', `${Math.ceil((chipH * swell) / 2) + 2}px`);\n  };\n  useLayoutEffect(() => {\n    const settle = () => {\n      measure();\n      apply(applied.current, true);\n    };\n    settle();\n    const observer = new ResizeObserver(settle);\n    if (groupRef.current) observer.observe(groupRef.current);\n    document.fonts?.ready.then(settle);\n    return () => observer.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [itemsKey, size, gap, swell, barge, shrink]);\n  useEffect(() => {\n    if (applied.current === at) return;\n    applied.current = at;\n    apply(at, true);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [at]);\n  useEffect(\n    () => () =>\n      mvs.current.forEach(mv => {\n        mv.x.destroy();\n        mv.sx.destroy();\n        mv.sy.destroy();\n      }),\n    []\n  );\n\n  const commit = (i, instant) => {\n    if (disabled || i === at || !list[i] || list[i].disabled) return;\n    applied.current = i;\n    apply(i, instant);\n    if (value === undefined) setInner(list[i].value);\n    onChange?.(list[i].value, i);\n  };\n  const stepFrom = (i, dir) => {\n    const n = list.length;\n    let j = i;\n    for (let tries = 0; tries < n; tries++) {\n      j = (j + dir + n) % n;\n      if (!list[j].disabled) return j;\n    }\n    return i;\n  };\n  const onKeyDown = (e, i) => {\n    let next = null;\n    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = stepFrom(i, 1);\n    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = stepFrom(i, -1);\n    else if (e.key === 'Home') next = stepFrom(-1, 1);\n    else if (e.key === 'End') next = stepFrom(list.length, -1);\n    else if (e.key === ' ' || e.key === 'Enter') next = i;\n    if (next === null) return;\n    e.preventDefault();\n    commit(next, true);\n    chipRefs.current[next]?.focus();\n  };\n\n  return (\n    <div\n      ref={groupRef}\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      data-disabled={disabled ? '' : undefined}\n      className={`group inline-flex items-center select-none [-webkit-touch-callout:none] gap-[var(--jr-gap)] px-[var(--jr-pad-x)] py-[var(--jr-pad-y)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50${className ? ` ${className}` : ''}`}\n      style={{\n        '--jr-chip': chipColor,\n        '--jr-active': activeColor,\n        '--jr-text': textColor,\n        '--jr-active-text': activeTextColor,\n        '--jr-gap': `${gap}px`,\n        '--jr-radius': `${radius}px`,\n        '--jr-h': `${h}px`,\n        '--jr-font': `${font}px`,\n        '--jr-px': `${px}px`\n      }}\n    >\n      {list.map((it, i) => (\n        <Chip\n          key={it.value}\n          mv={mvFor(i)}\n          ref={el => {\n            chipRefs.current[i] = el;\n          }}\n          type=\"button\"\n          role=\"radio\"\n          aria-checked={i === at}\n          tabIndex={i === at ? 0 : -1}\n          disabled={disabled || !!it.disabled}\n          className=\"group/chip relative m-0 cursor-pointer touch-manipulation border-0 bg-transparent p-0 text-inherit outline-none [font:inherit] origin-center [-webkit-tap-highlight-color:transparent] data-[on=true]:cursor-default disabled:cursor-default disabled:opacity-40 group-data-[disabled]:disabled:opacity-100\"\n          data-on={i === at ? 'true' : 'false'}\n          onClick={e => commit(i, e.detail === 0)}\n          onKeyDown={e => onKeyDown(e, i)}\n        >\n          <span className=\"relative inline-flex items-center justify-center gap-[0.4em] overflow-hidden rounded-[var(--jr-radius)] bg-[var(--jr-chip)] leading-none font-medium whitespace-nowrap [color:var(--jr-text)] [height:var(--jr-h)] [padding:0_var(--jr-px)] [font-size:var(--jr-font)] [transition:transform_160ms_cubic-bezier(0.23,1,0.32,1),background-color_200ms_ease,color_200ms_ease] group-active/chip:scale-[0.97] motion-reduce:group-active/chip:scale-100 group-data-[on=true]/chip:bg-[var(--jr-active)] group-data-[on=true]/chip:[color:var(--jr-active-text)] before:pointer-events-none before:absolute before:inset-0 before:bg-[var(--jr-text)] before:opacity-0 before:[transition:opacity_160ms_ease] before:content-[''] [@media(hover:hover)_and_(pointer:fine)]:group-hover/chip:group-enabled/chip:group-data-[on=false]/chip:before:opacity-[0.11]\">\n            {it.icon ? <span className=\"inline-flex\">{it.icon}</span> : null}\n            <span className=\"jelly-radio__label\">{it.label}</span>\n          </span>\n        </Chip>\n      ))}\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}