{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "BellToggle-TS-TW",
	"title": "BellToggle",
	"description": "Pill toggle where a press rings the bell, crossfades the label and unfurls the pill.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "BellToggle/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';\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={`group relative inline-grid grid-cols-[max-content] select-none [-webkit-touch-callout:none] [transition:transform_160ms_cubic-bezier(0.23,1,0.32,1)] data-[pressed]:scale-[0.97] data-[disabled]:opacity-55 data-[disabled]:data-[pressed]:scale-100 motion-reduce:data-[pressed]:scale-100${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=\"m-0 inline-flex cursor-pointer touch-manipulation items-center border-0 leading-none font-medium tracking-[0.01em] whitespace-nowrap outline-none [-webkit-tap-highlight-color:transparent] [font-family:inherit] [grid-area:1/1] [gap:var(--bt-gap)] [height:var(--bt-h)] [padding:0_var(--bt-px)] [border-radius:var(--bt-radius)] [background:var(--bt-bg)] [color:var(--bt-color)] [font-size:var(--bt-fs)] [clip-path:inset(0_var(--bt-clip)_0_0_round_var(--bt-radius))] [transition:background-color_var(--bt-fade)_ease,color_var(--bt-fade)_ease] disabled:cursor-default group-data-[on=true]:[background:var(--bt-on-bg)] group-data-[on=true]:[color:var(--bt-on-color)] [@media(hover:hover)_and_(pointer:fine)]:enabled:hover:[background:color-mix(in_srgb,var(--bt-color)_6%,var(--bt-bg))] [@media(hover:hover)_and_(pointer:fine)]:group-data-[on=true]:enabled:hover:[background:color-mix(in_srgb,var(--bt-on-color)_6%,var(--bt-on-bg))]\"\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\n          className=\"relative inline-grid flex-none [width:var(--bt-icon)] [height:var(--bt-icon)]\"\n          aria-hidden=\"true\"\n        >\n          <span\n            ref={glyphRef}\n            className=\"inline-grid place-items-center [grid-area:1/1] [width:var(--bt-icon)] [height:var(--bt-icon)] [transform-origin:50%_var(--bt-pivot)] [&_svg]:h-full [&_svg]:w-full\"\n          >\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\n              ref={clapperRef}\n              className=\"pointer-events-none absolute inset-0 [transform-origin:50%_var(--bt-pivot)] [&_svg]:block [&_svg]:h-full [&_svg]:w-full\"\n            >\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\n                ref={waveLeft}\n                className=\"pointer-events-none absolute -top-[3px] right-[calc(100%-2px)] h-3.5 w-3.5 origin-bottom-right fill-none stroke-current opacity-0 [stroke-linecap:round] [stroke-width:1.6]\"\n                viewBox=\"0 0 14 14\"\n              >\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\n                ref={waveRight}\n                className=\"pointer-events-none absolute -top-[3px] left-[calc(100%-2px)] h-3.5 w-3.5 origin-bottom-left fill-none stroke-current opacity-0 [stroke-linecap:round] [stroke-width:1.6]\"\n                viewBox=\"0 0 14 14\"\n              >\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\n              className=\"pointer-events-none absolute -top-1.5 -right-[7px] box-border grid h-3.5 min-w-3.5 place-items-center rounded-[7px] px-[3px] text-[9.5px] leading-none font-semibold opacity-0 [background:var(--bt-badge)] [color:var(--bt-badge-ink)] [transform:translateY(5px)_scale(0.6)] [transition:transform_180ms_cubic-bezier(0.23,1,0.32,1),opacity_140ms_ease] data-[show]:opacity-100 data-[show]:[transform:translateY(0)_scale(1)] data-[show]:[transition:transform_280ms_cubic-bezier(0.34,1.56,0.64,1),opacity_120ms_ease] motion-reduce:[transform:none]! motion-reduce:[transition:opacity_140ms_ease]!\"\n              data-show={showBadge ? '' : undefined}\n            >\n              <span\n                key={count}\n                className=\"block translate-y-0 opacity-100 [transition:opacity_200ms_ease,transform_200ms_cubic-bezier(0.23,1,0.32,1)] starting:translate-y-[5px] starting:opacity-0 motion-reduce:[transform:none]! motion-reduce:[transition:opacity_200ms_ease]\"\n              >\n                {count > 9 ? '9+' : count}\n              </span>\n            </span>\n          ) : null}\n        </span>\n        <span className=\"inline-grid h-[18px] grid-cols-[max-content] leading-[18px]\" aria-hidden=\"true\">\n          <span\n            ref={offRef}\n            className=\"justify-self-start opacity-0 blur-[2px] [grid-area:1/1] [transition:opacity_var(--bt-fade)_ease,filter_var(--bt-fade)_ease] group-data-[on=false]:opacity-100 group-data-[on=false]:blur-0 motion-reduce:blur-none! motion-reduce:[transition:opacity_var(--bt-fade)_ease]\"\n          >\n            {offLabel}\n          </span>\n          <span\n            ref={onRef}\n            className=\"justify-self-start opacity-0 blur-[2px] [grid-area:1/1] [transition:opacity_var(--bt-fade)_ease,filter_var(--bt-fade)_ease] group-data-[on=true]:opacity-100 group-data-[on=true]:blur-0 motion-reduce:blur-none! motion-reduce:[transition:opacity_var(--bt-fade)_ease]\"\n          >\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"
	]
}