{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "BellToggle-TS-CSS",
	"title": "BellToggle",
	"description": "Pill toggle that answers a press at three tempos: the bell rings on damped keyframes, the label blur-crossfades, and the pill unfurls to the longer label through a clip-path on a critically damped spring. The pressed state is the receipt.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "BellToggle.css",
			"target": "@components/BellToggle.css",
			"content": ".bell-toggle {\n  --bt-clip: 0px;\n  --bt-color: #f5f5f5;\n  --bt-bg: #27272a;\n  --bt-on-color: #18181b;\n  --bt-on-bg: #f5f5f5;\n  --bt-badge: #ef4444;\n  --bt-badge-ink: #ffffff;\n  --bt-radius: 22px;\n  --bt-fade: 200ms;\n  --bt-pivot: 16%;\n  --bt-h: 44px;\n  --bt-fs: 13.5px;\n  --bt-icon: 16px;\n  --bt-px: 19px;\n  --bt-gap: 9px;\n  --bt-press: 0.97;\n  --bt-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  position: relative;\n  display: inline-grid;\n  grid-template-columns: max-content;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  transition: transform 160ms var(--bt-ease-out);\n}\n\n.bell-toggle[data-pressed] {\n  transform: scale(var(--bt-press));\n}\n\n.bell-toggle[data-disabled] {\n  opacity: 0.55;\n}\n\n.bell-toggle[data-disabled][data-pressed] {\n  transform: none;\n}\n\n.bell-toggle__button {\n  grid-area: 1 / 1;\n  display: inline-flex;\n  align-items: center;\n  gap: var(--bt-gap);\n  height: var(--bt-h);\n  margin: 0;\n  padding: 0 var(--bt-px);\n  border: 0;\n  border-radius: var(--bt-radius);\n  background: var(--bt-bg);\n  color: var(--bt-color);\n  font: inherit;\n  font-size: var(--bt-fs);\n  font-weight: 500;\n  line-height: 1;\n  letter-spacing: 0.01em;\n  white-space: nowrap;\n  cursor: pointer;\n  outline: none;\n  touch-action: manipulation;\n  -webkit-tap-highlight-color: transparent;\n  clip-path: inset(0 var(--bt-clip) 0 0 round var(--bt-radius));\n  transition:\n    background-color var(--bt-fade) ease,\n    color var(--bt-fade) ease;\n}\n\n.bell-toggle__button:disabled {\n  cursor: default;\n}\n\n.bell-toggle[data-on='true'] .bell-toggle__button {\n  background: var(--bt-on-bg);\n  color: var(--bt-on-color);\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .bell-toggle__button:enabled:hover {\n    background: color-mix(in srgb, var(--bt-color) 6%, var(--bt-bg));\n  }\n\n  .bell-toggle[data-on='true'] .bell-toggle__button:enabled:hover {\n    background: color-mix(in srgb, var(--bt-on-color) 6%, var(--bt-on-bg));\n  }\n}\n\n.bell-toggle__bell {\n  position: relative;\n  display: inline-grid;\n  flex: none;\n  width: var(--bt-icon);\n  height: var(--bt-icon);\n}\n\n.bell-toggle__glyph {\n  display: inline-grid;\n  place-items: center;\n  grid-area: 1 / 1;\n  width: var(--bt-icon);\n  height: var(--bt-icon);\n  transform-origin: 50% var(--bt-pivot);\n}\n\n.bell-toggle__glyph svg {\n  width: 100%;\n  height: 100%;\n}\n\n.bell-toggle__clapper {\n  position: absolute;\n  inset: 0;\n  transform-origin: 50% var(--bt-pivot);\n  pointer-events: none;\n}\n\n.bell-toggle__clapper svg {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n.bell-toggle__wave {\n  position: absolute;\n  top: -3px;\n  width: 14px;\n  height: 14px;\n  fill: none;\n  stroke: currentColor;\n  stroke-width: 1.6;\n  stroke-linecap: round;\n  opacity: 0;\n  pointer-events: none;\n}\n\n.bell-toggle__wave--left {\n  right: calc(100% - 2px);\n  transform-origin: 100% 100%;\n}\n\n.bell-toggle__wave--right {\n  left: calc(100% - 2px);\n  transform-origin: 0% 100%;\n}\n\n.bell-toggle__badge {\n  position: absolute;\n  top: -6px;\n  right: -7px;\n  display: grid;\n  place-items: center;\n  min-width: 14px;\n  height: 14px;\n  padding: 0 3px;\n  box-sizing: border-box;\n  border-radius: 7px;\n  background: var(--bt-badge);\n  color: var(--bt-badge-ink);\n  font-size: 9.5px;\n  font-weight: 600;\n  line-height: 1;\n  opacity: 0;\n  transform: translateY(5px) scale(0.6);\n  pointer-events: none;\n  transition:\n    transform 180ms var(--bt-ease-out),\n    opacity 140ms ease;\n}\n\n.bell-toggle__badge[data-show] {\n  opacity: 1;\n  transform: translateY(0) scale(1);\n  transition:\n    transform 280ms cubic-bezier(0.34, 1.56, 0.64, 1),\n    opacity 120ms ease;\n}\n\n.bell-toggle__digit {\n  display: block;\n  opacity: 1;\n  transform: translateY(0);\n  transition:\n    opacity 200ms ease,\n    transform 200ms var(--bt-ease-out);\n}\n\n@starting-style {\n  .bell-toggle__digit {\n    opacity: 0;\n    transform: translateY(5px);\n  }\n}\n\n.bell-toggle__say {\n  display: inline-grid;\n  grid-template-columns: max-content;\n  height: 18px;\n  line-height: 18px;\n}\n\n.bell-toggle__face {\n  grid-area: 1 / 1;\n  justify-self: start;\n  opacity: 0;\n  filter: blur(2px);\n  transition:\n    opacity var(--bt-fade) ease,\n    filter var(--bt-fade) ease;\n}\n\n.bell-toggle[data-on='false'] .bell-toggle__face--off,\n.bell-toggle[data-on='true'] .bell-toggle__face--on {\n  opacity: 1;\n  filter: blur(0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .bell-toggle[data-pressed] {\n    transform: none;\n  }\n\n  .bell-toggle__face {\n    filter: none !important;\n    transition: opacity var(--bt-fade) ease;\n  }\n\n  .bell-toggle__badge,\n  .bell-toggle__badge[data-show] {\n    transition: opacity 140ms ease;\n    transform: none;\n  }\n\n  .bell-toggle__digit {\n    transition: opacity 200ms ease;\n    transform: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "BellToggle.tsx",
			"content": "'use client';\n\nimport React, { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type AnimationPlaybackControls,\n  type MotionStyle\n} from 'motion/react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { Notification03Icon } from '@hugeicons/core-free-icons';\nimport './BellToggle.css';\n\nexport type BellToggleSize = 'sm' | 'md' | 'lg';\n\nexport interface BellToggleProps {\n  offLabel?: string;\n  onLabel?: string;\n  icon?: ReactNode;\n  label?: string;\n  color?: string;\n  background?: string;\n  onColor?: string;\n  onBackground?: string;\n  size?: BellToggleSize;\n  radius?: number;\n  ringAmplitude?: number;\n  ringPasses?: number;\n  ringDecay?: number;\n  ringDuration?: number;\n  ringPivot?: number;\n  crossfadeMs?: number;\n  revealBounce?: number;\n  count?: number;\n  badge?: boolean;\n  badgeColor?: string;\n  badgeTextColor?: string;\n  waves?: boolean;\n  clapper?: boolean;\n  pressed?: boolean;\n  defaultPressed?: boolean;\n  onChange?: (pressed: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n}\n\nconst SPRING_UI = { type: 'spring' as const, duration: 0.3, bounce: 0 };\nconst SEG_EASE = 'cubic-bezier(0.77, 0, 0.175, 1)';\nconst WARP = 0.6;\nconst SIZES: Record<string, [number, number, number, number, number]> = {\n  sm: [36, 12.5, 14, 15, 8],\n  md: [44, 13.5, 16, 19, 9],\n  lg: [52, 15, 18, 23, 10]\n};\nconst WOBBLE = { amplitude: 0.4, passes: 3, duration: 420 };\nconst BELL_BODY = 'M6 16.5V10a6 6 0 0 1 12 0v6.5l1.6 2.3H4.4L6 16.5z';\n\nconst passOffset = (k: number, passes: number) => 1 - Math.pow(1 - (k + 2 / 3) / (passes + 1), WARP);\nconst ringKeyframes = (from: number, amplitude: number, passes: number, decay: number): Keyframe[] => {\n  const frames: Keyframe[] = [{ transform: `rotate(${from}deg)`, offset: 0, easing: SEG_EASE }];\n  for (let k = 0; k < passes; k++) {\n    const angle = amplitude * Math.pow(1 - k / passes, decay) * (k % 2 ? 1 : -1);\n    frames.push({ transform: `rotate(${angle.toFixed(2)}deg)`, offset: passOffset(k, passes), easing: SEG_EASE });\n  }\n  frames.push({ transform: 'rotate(0deg)', offset: 1 });\n  return frames;\n};\nconst liveAngle = (el: Element) => {\n  const tf = getComputedStyle(el).transform;\n  if (!tf || tf === 'none') return 0;\n  const m = new DOMMatrix(tf);\n  return (Math.atan2(m.b, m.a) * 180) / Math.PI;\n};\n\nconst BellToggle: React.FC<BellToggleProps> = ({\n  offLabel = 'Notify me',\n  onLabel = \"You'll be notified\",\n  icon,\n  label,\n  color = '#f5f5f5',\n  background = '#27272a',\n  onColor = '#18181b',\n  onBackground = '#f5f5f5',\n  size = 'md',\n  radius = 22,\n  ringAmplitude = 17,\n  ringPasses = 5,\n  ringDecay = 1,\n  ringDuration = 820,\n  ringPivot = 16,\n  crossfadeMs = 200,\n  revealBounce = 0,\n  count = 0,\n  badge = true,\n  badgeColor = '#ef4444',\n  badgeTextColor = '#ffffff',\n  waves = true,\n  clapper = false,\n  pressed,\n  defaultPressed = false,\n  onChange,\n  disabled = false,\n  className = ''\n}) => {\n  const [inner, setInner] = useState(defaultPressed);\n  const on = pressed ?? inner;\n  const reduce = useReducedMotion();\n  const rootRef = useRef<HTMLSpanElement>(null);\n  const glyphRef = useRef<HTMLSpanElement>(null);\n  const clapperRef = useRef<HTMLSpanElement>(null);\n  const waveLeft = useRef<SVGSVGElement>(null);\n  const waveRight = useRef<SVGSVGElement>(null);\n  const offRef = useRef<HTMLSpanElement>(null);\n  const onRef = useRef<HTMLSpanElement>(null);\n  const lastInput = useRef<'pointer' | 'keyboard'>('pointer');\n  const pending = useRef<'pointer' | 'keyboard' | null>(null);\n  const spring = useRef<AnimationPlaybackControls | null>(null);\n  const prevCount = useRef(count);\n  const [h, fs, iconSize, px, gap] = SIZES[size] ?? SIZES.md;\n\n  const t = useMotionValue(on ? 1 : 0);\n  const wOff = useMotionValue(0);\n  const wOn = useMotionValue(0);\n  const clip = useTransform([t, wOff, wOn], ([v, a, b]: number[]) => `${Math.max(a, b) - (a + (b - a) * v)}px`);\n\n  useLayoutEffect(() => {\n    const measure = () => {\n      if (offRef.current) wOff.set(offRef.current.offsetWidth);\n      if (onRef.current) wOn.set(onRef.current.offsetWidth);\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    if (offRef.current) observer.observe(offRef.current);\n    if (onRef.current) observer.observe(onRef.current);\n    document.fonts?.ready.then(measure);\n    return () => observer.disconnect();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [offLabel, onLabel, size]);\n\n  const swing = (amplitude: number, passes: number, duration: number) => {\n    const el = glyphRef.current;\n    if (!el) return;\n    el.getAnimations().forEach(a => a.cancel());\n    el.animate(ringKeyframes(liveAngle(el), amplitude, passes, ringDecay), { duration, easing: 'linear' });\n    const c = clapperRef.current;\n    if (c) {\n      c.getAnimations().forEach(a => a.cancel());\n      c.animate(ringKeyframes(liveAngle(c), amplitude * 1.6, passes, ringDecay), {\n        duration,\n        delay: 70,\n        easing: 'linear'\n      });\n    }\n    if (!waves) return;\n    for (let k = 0; k < passes; k++) {\n      const side = k % 2 ? waveRight.current : waveLeft.current;\n      if (!side) continue;\n      const strength = Math.pow(1 - k / passes, ringDecay);\n      side.animate(\n        [\n          { opacity: 0, transform: 'scale(0.55)' },\n          { opacity: 0.9 * strength, offset: 0.3 },\n          { opacity: 0, transform: 'scale(1.25)' }\n        ],\n        { duration: 380, delay: passOffset(k, passes) * duration, easing: 'ease-out' }\n      );\n    }\n  };\n\n  useLayoutEffect(() => {\n    const pointer = pending.current === 'pointer' && !reduce;\n    pending.current = null;\n    spring.current?.stop();\n    if (pointer) spring.current = animate(t, on ? 1 : 0, { ...SPRING_UI, bounce: revealBounce });\n    else t.jump(on ? 1 : 0);\n    if (on && pointer) swing(ringAmplitude, ringPasses, ringDuration);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [on]);\n  useLayoutEffect(() => () => spring.current?.stop(), []);\n\n  useEffect(() => {\n    const was = prevCount.current;\n    prevCount.current = count;\n    if (count > was && on && !reduce) swing(ringAmplitude * WOBBLE.amplitude, WOBBLE.passes, WOBBLE.duration);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [count]);\n\n  const toggle = () => {\n    pending.current = lastInput.current;\n    if (pressed === undefined) setInner(!on);\n    onChange?.(!on);\n  };\n  const press = (e: React.PointerEvent<HTMLButtonElement>) => {\n    lastInput.current = 'pointer';\n    if (e.button === 0 && !disabled && rootRef.current) rootRef.current.dataset.pressed = '';\n  };\n  const release = () => {\n    if (rootRef.current) delete rootRef.current.dataset.pressed;\n  };\n  const key = (e: React.KeyboardEvent<HTMLButtonElement>) => {\n    if (e.key === 'Enter' || e.key === ' ') lastInput.current = 'keyboard';\n  };\n\n  const showBadge = badge && on && count > 0;\n\n  return (\n    <motion.span\n      ref={rootRef}\n      className={`bell-toggle${className ? ` ${className}` : ''}`}\n      data-on={on ? 'true' : 'false'}\n      data-disabled={disabled ? '' : undefined}\n      style={\n        {\n          '--bt-clip': clip,\n          '--bt-color': color,\n          '--bt-bg': background,\n          '--bt-on-color': onColor,\n          '--bt-on-bg': onBackground,\n          '--bt-badge': badgeColor,\n          '--bt-badge-ink': badgeTextColor,\n          '--bt-radius': `${radius}px`,\n          '--bt-fade': `${crossfadeMs}ms`,\n          '--bt-pivot': `${ringPivot}%`,\n          '--bt-h': `${h}px`,\n          '--bt-fs': `${fs}px`,\n          '--bt-icon': `${iconSize}px`,\n          '--bt-px': `${px}px`,\n          '--bt-gap': `${gap}px`\n        } as MotionStyle & CSSProperties\n      }\n    >\n      <button\n        type=\"button\"\n        className=\"bell-toggle__button\"\n        aria-pressed={on}\n        aria-label={label ?? offLabel}\n        disabled={disabled}\n        onPointerDown={press}\n        onPointerUp={release}\n        onPointerCancel={release}\n        onPointerLeave={release}\n        onKeyDown={key}\n        onClick={toggle}\n      >\n        <span className=\"bell-toggle__bell\" aria-hidden=\"true\">\n          <span ref={glyphRef} className=\"bell-toggle__glyph\">\n            {clapper ? (\n              <svg\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <path d={BELL_BODY} />\n                <path d=\"M12 2.5V4\" />\n              </svg>\n            ) : (\n              (icon ?? <HugeiconsIcon icon={Notification03Icon} size={iconSize} strokeWidth={2} />)\n            )}\n          </span>\n          {clapper ? (\n            <span ref={clapperRef} className=\"bell-toggle__clapper\">\n              <svg viewBox=\"0 0 24 24\">\n                <circle cx=\"12\" cy=\"20.4\" r=\"1.7\" fill=\"currentColor\" />\n              </svg>\n            </span>\n          ) : null}\n          {waves ? (\n            <>\n              <svg ref={waveLeft} className=\"bell-toggle__wave bell-toggle__wave--left\" viewBox=\"0 0 14 14\">\n                <path d=\"M14 8a6 6 0 0 0-6 6\" />\n                <path d=\"M14 4A10 10 0 0 0 4 14\" />\n              </svg>\n              <svg ref={waveRight} className=\"bell-toggle__wave bell-toggle__wave--right\" viewBox=\"0 0 14 14\">\n                <path d=\"M0 8a6 6 0 0 1 6 6\" />\n                <path d=\"M0 4a10 10 0 0 1 10 10\" />\n              </svg>\n            </>\n          ) : null}\n          {badge ? (\n            <span className=\"bell-toggle__badge\" data-show={showBadge ? '' : undefined}>\n              <span key={count} className=\"bell-toggle__digit\">\n                {count > 9 ? '9+' : count}\n              </span>\n            </span>\n          ) : null}\n        </span>\n        <span className=\"bell-toggle__say\" aria-hidden=\"true\">\n          <span ref={offRef} className=\"bell-toggle__face bell-toggle__face--off\">\n            {offLabel}\n          </span>\n          <span ref={onRef} className=\"bell-toggle__face bell-toggle__face--on\">\n            {onLabel}\n          </span>\n        </span>\n      </button>\n    </motion.span>\n  );\n};\n\nexport default BellToggle;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}