{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox",
  "title": "Checkbox",
  "description": "A checkbox with drawn check animation, indeterminate state, and hidden native input for forms.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/checkbox.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  cloneElement,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useId,\n  useState,\n  type ReactNode,\n} from 'react'\nimport { motion, useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { springs } from '@/lib/motion-tokens'\n\nexport type CheckedState = boolean | 'indeterminate'\n\nexport interface CheckboxProps {\n  id?: string\n  checked?: CheckedState\n  defaultChecked?: boolean\n  onCheckedChange?: (checked: CheckedState) => void\n  name?: string\n  disabled?: boolean\n  required?: boolean\n  'aria-invalid'?: boolean | 'true' | 'false' | 'grammar' | 'spelling'\n  'aria-label'?: string\n  'aria-labelledby'?: string\n  'aria-describedby'?: string\n  className?: string\n  value?: string\n}\n\nexport interface CheckboxFieldProps {\n  label: string\n  description?: string\n  strikeThrough?: boolean\n  className?: string\n  children: ReactNode\n}\n\nfunction useControlledChecked(\n  controlledValue: CheckedState | undefined,\n  defaultValue: boolean | undefined,\n  onChange: ((checked: CheckedState) => void) | undefined,\n): [CheckedState, (next: CheckedState) => void] {\n  const [internal, setInternal] = useState<CheckedState>(defaultValue ?? false)\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internal\n\n  const setValue = useCallback(\n    (next: CheckedState) => {\n      if (!isControlled) setInternal(next)\n      onChange?.(next)\n    },\n    [isControlled, onChange],\n  )\n\n  return [value, setValue]\n}\n\nconst CHECK_PATH = 'M3.5 8.5L7 12L12.5 5.5'\nconst DASH_PATH = 'M4 8L12 8'\n\nfunction CheckGlyph({ state, animate }: { state: CheckedState; animate: boolean }) {\n  const reduceMotion = useReducedMotion()\n  const isChecked = state === true\n  const isIndeterminate = state === 'indeterminate'\n  const shouldShow = isChecked || isIndeterminate\n\n  if (!shouldShow) return null\n\n  const path = isIndeterminate ? DASH_PATH : CHECK_PATH\n  const shouldAnimate = animate && !reduceMotion\n\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"absolute inset-0 size-full\" aria-hidden=\"true\">\n      <motion.path\n        d={path}\n        stroke=\"currentColor\"\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        initial={shouldAnimate ? { pathLength: 0, opacity: 0 } : { pathLength: 1, opacity: 1 }}\n        animate={{ pathLength: 1, opacity: 1 }}\n        exit={shouldAnimate ? { pathLength: 0, opacity: 0 } : undefined}\n        transition={shouldAnimate ? springs.fast : { duration: 0 }}\n      />\n    </svg>\n  )\n}\n\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(\n  (\n    {\n      id,\n      checked: controlledChecked,\n      defaultChecked,\n      onCheckedChange,\n      name,\n      disabled = false,\n      required = false,\n      'aria-invalid': ariaInvalid,\n      'aria-label': ariaLabel,\n      'aria-labelledby': ariaLabelledBy,\n      'aria-describedby': ariaDescribedBy,\n      className,\n      value = 'on',\n    },\n    forwardedRef,\n  ) => {\n    const [checkedState, setCheckedState] = useControlledChecked(\n      controlledChecked,\n      defaultChecked,\n      onCheckedChange,\n    )\n\n    const [toggleCount, setToggleCount] = useState(0)\n\n    const isChecked = checkedState === true\n    const isIndeterminate = checkedState === 'indeterminate'\n\n    const ariaCheckedValue = isIndeterminate ? 'mixed' : isChecked\n\n    const handleClick = useCallback(() => {\n      if (disabled) return\n      const next: CheckedState = isIndeterminate ? true : !isChecked\n      setCheckedState(next)\n      setToggleCount((c) => c + 1)\n    }, [disabled, isIndeterminate, isChecked, setCheckedState])\n\n    const handleKeyDown = useCallback(\n      (e: React.KeyboardEvent<HTMLButtonElement>) => {\n        if (e.key === ' ' || e.key === 'Enter') {\n          e.preventDefault()\n          handleClick()\n        }\n      },\n      [handleClick],\n    )\n\n    const handlePointerDown = useCallback((e: React.PointerEvent<HTMLButtonElement>) => {\n      e.preventDefault()\n    }, [])\n\n    return (\n      <>\n        <button\n          id={id}\n          type=\"button\"\n          role=\"checkbox\"\n          aria-checked={ariaCheckedValue}\n          aria-label={ariaLabel}\n          aria-labelledby={ariaLabelledBy}\n          aria-invalid={ariaInvalid}\n          aria-describedby={ariaDescribedBy}\n          aria-disabled={disabled || undefined}\n          disabled={disabled}\n          aria-required={required || undefined}\n          onClick={handleClick}\n          onKeyDown={handleKeyDown}\n          onPointerDown={handlePointerDown}\n          className={cn(\n            'relative inline-flex size-5 shrink-0 items-center justify-center rounded-[5px] supports-[corner-shape:squircle]:corner-squircle',\n            'border border-(--color-border)',\n            'transition-colors duration-(--motion-dur-fast) ease-(--motion-ease-out) motion-reduce:transition-none',\n            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-(--color-bg)',\n            'disabled:cursor-not-allowed disabled:opacity-50',\n            (isChecked || isIndeterminate) &&\n              'border-transparent bg-(--color-fg) text-(--color-bg)',\n            !(isChecked || isIndeterminate) && 'bg-(--color-bg)',\n            ariaInvalid && 'border-destructive',\n            className,\n          )}\n        >\n          <CheckGlyph state={checkedState} animate={toggleCount > 0} />\n        </button>\n        <input\n          ref={forwardedRef}\n          type=\"checkbox\"\n          name={name}\n          value={value}\n          checked={isChecked}\n          disabled={disabled}\n          required={required}\n          aria-hidden=\"true\"\n          tabIndex={-1}\n          onChange={() => {}}\n          className=\"pointer-events-none absolute size-0 opacity-0\"\n          style={{ position: 'absolute', width: 0, height: 0 }}\n        />\n      </>\n    )\n  },\n)\nCheckbox.displayName = 'Checkbox'\n\nexport function CheckboxField({\n  label,\n  description,\n  strikeThrough = false,\n  className,\n  children,\n}: CheckboxFieldProps) {\n  const generatedId = useId()\n  const labelId = `${generatedId}-label`\n  const descriptionId = description ? `${generatedId}-description` : undefined\n  const checkbox = isValidElement<CheckboxProps>(children) ? children : null\n  const controlId = checkbox?.props.id ?? `${generatedId}-control`\n  const describedBy = [checkbox?.props['aria-describedby'], descriptionId].filter(Boolean).join(' ')\n  const control = checkbox\n    ? cloneElement(checkbox, {\n        id: controlId,\n        'aria-labelledby':\n          checkbox.props['aria-labelledby'] ?? (checkbox.props['aria-label'] ? undefined : labelId),\n        'aria-describedby': describedBy || undefined,\n      })\n    : children\n\n  const isChecked = checkbox?.props.checked === true\n  const reduceMotion = useReducedMotion()\n\n  return (\n    <div className={cn('flex min-h-11 items-start gap-3', className)}>\n      {control}\n      <div className=\"flex flex-col gap-0.5\">\n        <label\n          id={labelId}\n          htmlFor={controlId}\n          className=\"cursor-pointer text-base font-medium leading-tight select-none\"\n        >\n          {strikeThrough ? (\n            <span\n              className={cn(\n                'relative inline-block transition-colors duration-(--motion-dur-base) ease-(--motion-ease-out) motion-reduce:transition-none',\n                isChecked ? 'text-(--color-muted)' : 'text-(--color-fg)',\n              )}\n            >\n              {label}\n              <motion.span\n                aria-hidden=\"true\"\n                className=\"absolute left-0 right-0 top-1/2 h-px bg-current motion-reduce:transition-none\"\n                style={{ transformOrigin: 'left' }}\n                initial={false}\n                animate={{ scaleX: isChecked ? 1 : 0 }}\n                transition={reduceMotion ? { duration: 0 } : springs.settle}\n              />\n            </span>\n          ) : (\n            <span className=\"text-(--color-fg)\">{label}</span>\n          )}\n        </label>\n        {description && (\n          <p id={descriptionId} className=\"text-sm leading-snug text-(--color-muted)\">\n            {description}\n          </p>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport function CheckboxPreview() {\n  const [todoChecked, setTodoChecked] = useState(false)\n\n  return (\n    <div className=\"flex flex-col gap-4 p-8\">\n      <Checkbox defaultChecked />\n      <Checkbox />\n      <Checkbox checked=\"indeterminate\" />\n      <Checkbox disabled defaultChecked />\n      <CheckboxField label=\"Buy groceries\" strikeThrough>\n        <Checkbox checked={todoChecked} onCheckedChange={(next) => setTodoChecked(next === true)} />\n      </CheckboxField>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/motion-tokens.ts",
      "type": "registry:lib",
      "content": "export const durations = {\n  instant: '50ms',\n  fast: '120ms',\n  base: '200ms',\n  slow: '320ms',\n} as const\n\nexport const easings = {\n  out: 'cubic-bezier(0.22, 1, 0.36, 1)',\n  inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',\n} as const\n\nexport const springs = {\n  fast: { type: 'spring', stiffness: 400, damping: 30, mass: 0.8 },\n  press: { type: 'spring', stiffness: 700, damping: 32, mass: 1 },\n  moderate: { type: 'spring', stiffness: 300, damping: 24, mass: 1 },\n  settle: { type: 'spring', stiffness: 260, damping: 26, mass: 1 },\n} as const\n"
    },
    {
      "path": "lib/sound.ts",
      "type": "registry:lib",
      "content": "'use client'\n\nconst MUTE_KEY = 'sound-muted'\nconst MUTE_EVENT = 'sound-mute-change'\n\nlet muted = false\nif (typeof window !== 'undefined') {\n  try {\n    muted = localStorage.getItem(MUTE_KEY) === '1'\n  } catch {}\n}\n\nexport function isSoundMuted(): boolean {\n  return muted\n}\n\nexport function setSoundMuted(value: boolean) {\n  muted = value\n  if (typeof window !== 'undefined') {\n    try {\n      localStorage.setItem(MUTE_KEY, value ? '1' : '0')\n    } catch {}\n    window.dispatchEvent(new CustomEvent(MUTE_EVENT))\n  }\n}\n\nexport function toggleSoundMuted(): boolean {\n  setSoundMuted(!muted)\n  return muted\n}\n\nexport function subscribeSoundMuted(callback: () => void) {\n  if (typeof window === 'undefined') return () => {}\n  window.addEventListener(MUTE_EVENT, callback)\n  const onStorage = (e: StorageEvent) => {\n    if (e.key === MUTE_KEY) callback()\n  }\n  window.addEventListener('storage', onStorage)\n  return () => {\n    window.removeEventListener(MUTE_EVENT, callback)\n    window.removeEventListener('storage', onStorage)\n  }\n}\n\nlet audioCtx: AudioContext | null = null\n\nfunction initAudio(): AudioContext | null {\n  if (typeof window === 'undefined') return null\n  if (!audioCtx) {\n    const AudioContextClass =\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n    if (AudioContextClass) {\n      audioCtx = new AudioContextClass()\n    }\n  }\n  if (audioCtx && audioCtx.state === 'suspended') {\n    audioCtx.resume().catch(() => {})\n  }\n  return audioCtx\n}\n\nif (typeof window !== 'undefined') {\n  const unlock = () => {\n    initAudio()\n    window.removeEventListener('pointerdown', unlock)\n    window.removeEventListener('keydown', unlock)\n    window.removeEventListener('touchstart', unlock)\n  }\n  window.addEventListener('pointerdown', unlock, { passive: true })\n  window.addEventListener('keydown', unlock, { passive: true })\n  window.addEventListener('touchstart', unlock, { passive: true })\n}\n\nexport function getAudioContext(): AudioContext | null {\n  return initAudio()\n}\n\nexport function playHoverSound(volume = 0.12, pitch = 1.2) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1400 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(350 * pitch, now + 0.012)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.012)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.012)\n  } catch {}\n}\n\nexport function playClickSound(volume = 0.2, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'triangle'\n    osc.frequency.setValueAtTime(850 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(160 * pitch, now + 0.02)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.02)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.02)\n  } catch {}\n}\n\nexport function playTickSound(volume = 0.1, pitch = 1.5) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1800 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(450 * pitch, now + 0.01)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.01)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.01)\n  } catch {}\n}\n\nexport function playBounceSound(volume = 0.3, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(260 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(650 * pitch, now + 0.04)\n    osc.frequency.exponentialRampToValueAtTime(190 * pitch, now + 0.14)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.14)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.14)\n\n    const subOsc = ctx.createOscillator()\n    const subGain = ctx.createGain()\n\n    subOsc.type = 'triangle'\n    subOsc.frequency.setValueAtTime(420 * pitch, now)\n    subOsc.frequency.exponentialRampToValueAtTime(120 * pitch, now + 0.05)\n\n    subGain.gain.setValueAtTime(volume * 0.7, now)\n    subGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.05)\n\n    subOsc.connect(subGain)\n    subGain.connect(ctx.destination)\n\n    subOsc.start(now)\n    subOsc.stop(now + 0.05)\n  } catch {}\n}\n"
    }
  ],
  "type": "registry:ui"
}
