{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rabbit-mascot",
  "title": "Rabbit Mascot",
  "description": "An expressive rabbit boy mascot with round glasses and mood-driven face animations.",
  "dependencies": [
    "react"
  ],
  "files": [
    {
      "path": "registry/gammaui/rabbit-mascot.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { applyRabbitArmReach } from \"@/registry/gammaui/rabbit-mascot-arm\"\nimport { startRabbitBlinkLoop } from \"@/registry/gammaui/rabbit-mascot-blink\"\nimport {\n  applyRabbitMood,\n  type RabbitMood,\n} from \"@/registry/gammaui/rabbit-mascot-moods\"\n\ninterface RabbitMascotProps {\n  mood?: RabbitMood\n  className?: string\n  size?: number\n  blink?: boolean\n  armReach?: boolean\n  onClick?: () => void\n  onReady?: () => void\n}\n\nexport function RabbitMascot({\n  mood = \"neutral\",\n  className,\n  size = 280,\n  blink = true,\n  armReach = false,\n  onClick,\n  onReady,\n}: RabbitMascotProps) {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const moodRef = useRef(mood)\n  const onReadyRef = useRef(onReady)\n  const isLoadedRef = useRef(false)\n  const instanceId = useId().replace(/:/g, \"\")\n  const idPrefix = `rabbit-${instanceId}-`\n  const [isLoaded, setIsLoaded] = useState(false)\n\n  moodRef.current = mood\n  onReadyRef.current = onReady\n\n  useEffect(() => {\n    let cancelled = false\n\n    async function loadMascot() {\n      const response = await fetch(\"/rabbit.svg\")\n      if (!response.ok || cancelled || !containerRef.current) return\n\n      const markup = (await response.text()).replace(/_r_0_/g, idPrefix)\n      containerRef.current.innerHTML = markup\n\n      const svg = containerRef.current.querySelector(\"svg\")\n      if (!svg) return\n\n      svg.style.display = \"block\"\n      svg.style.overflow = \"visible\"\n      svg.style.width = \"100%\"\n      svg.style.height = \"100%\"\n      if (onClick) svg.style.cursor = \"pointer\"\n\n      applyRabbitMood(svg, moodRef.current)\n      applyRabbitArmReach(svg, armReach)\n\n      isLoadedRef.current = true\n      setIsLoaded(true)\n      onReadyRef.current?.()\n    }\n\n    loadMascot()\n\n    return () => {\n      cancelled = true\n      isLoadedRef.current = false\n    }\n  }, [idPrefix, onClick])\n\n  useEffect(() => {\n    if (!isLoaded || !containerRef.current) return\n    applyRabbitMood(containerRef.current, mood)\n  }, [isLoaded, mood])\n\n  useEffect(() => {\n    if (!isLoaded || !containerRef.current) return\n    applyRabbitArmReach(containerRef.current, armReach)\n  }, [isLoaded, armReach])\n\n  useEffect(() => {\n    if (!isLoaded || !blink || !containerRef.current) return\n\n    return startRabbitBlinkLoop(containerRef.current, () => moodRef.current)\n  }, [isLoaded, blink])\n\n  return (\n    <div\n      ref={containerRef}\n      role=\"img\"\n      aria-live=\"polite\"\n      aria-label={`Rabbit mascot, ${mood} mood`}\n      className={cn(\n        \"form-companion-mascot inline-block select-none\",\n        className\n      )}\n      style={{ width: size, height: size * (720 / 900) }}\n      onClick={onClick}\n      onKeyDown={\n        onClick\n          ? (event) => {\n              if (event.key === \"Enter\" || event.key === \" \") {\n                event.preventDefault()\n                onClick()\n              }\n            }\n          : undefined\n      }\n      tabIndex={onClick ? 0 : undefined}\n    />\n  )\n}\n\nexport {\n  RABBIT_MOODS,\n  RABBIT_MOOD_LABELS,\n  type RabbitMood,\n} from \"@/registry/gammaui/rabbit-mascot-moods\"\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/gammaui/rabbit-mascot-moods.ts",
      "content": "export const RABBIT_MOODS = [\n  \"neutral\",\n  \"happy\",\n  \"sad\",\n  \"love\",\n  \"winky\",\n  \"surprised\",\n  \"thinking\",\n  \"side-eye\",\n  \"shy\",\n] as const\n\nexport type RabbitMood = (typeof RABBIT_MOODS)[number]\n\nexport const RABBIT_MOOD_LABELS: Record<RabbitMood, string> = {\n  neutral: \"Neutral\",\n  happy: \"Happy\",\n  sad: \"Sad\",\n  love: \"Love\",\n  winky: \"Winky\",\n  surprised: \"Surprised\",\n  thinking: \"Thinking\",\n  \"side-eye\": \"Side eye\",\n  shy: \"Shy\",\n}\n\ninterface RabbitMoodLayers {\n  leftDefaultEye: number\n  rightDefaultEye: number\n  defaultMouth: number\n  sadMouth: number\n  leftHappyArc: number\n  rightHappyArc: number\n  passwordFace: number\n  shyFace: number\n  sideEye: number\n  hmmLids: number\n  sadBrows: number\n  happyOpenMouth: number\n  mouthOh: number\n  leftEyeStar: number\n  rightEyeStar: number\n  leftWantTwinkle: number\n  rightWantTwinkle: number\n  emotionFx: number\n  sadTears: number\n  happyTwinkle: number\n  cheekOpacity: number\n  leftWink: number\n  rightWink: number\n  spectacles: number\n}\n\nconst NEUTRAL_LAYERS: RabbitMoodLayers = {\n  leftDefaultEye: 1,\n  rightDefaultEye: 1,\n  defaultMouth: 1,\n  sadMouth: 0,\n  leftHappyArc: 0,\n  rightHappyArc: 0,\n  passwordFace: 0,\n  shyFace: 0,\n  sideEye: 0,\n  hmmLids: 0,\n  sadBrows: 0,\n  happyOpenMouth: 0,\n  mouthOh: 0,\n  leftEyeStar: 0,\n  rightEyeStar: 0,\n  leftWantTwinkle: 0,\n  rightWantTwinkle: 0,\n  emotionFx: 0,\n  sadTears: 0,\n  happyTwinkle: 0,\n  cheekOpacity: 0.65,\n  leftWink: 0,\n  rightWink: 0,\n  spectacles: 1,\n}\n\nconst MOOD_LAYER_MAP: Record<RabbitMood, Partial<RabbitMoodLayers>> = {\n  neutral: {},\n  happy: {\n    leftDefaultEye: 0,\n    rightDefaultEye: 0,\n    defaultMouth: 0,\n    leftHappyArc: 1,\n    rightHappyArc: 1,\n    happyOpenMouth: 1,\n    happyTwinkle: 1,\n    emotionFx: 1,\n    cheekOpacity: 0.85,\n  },\n  sad: {\n    sadBrows: 1,\n    sadTears: 1,\n    emotionFx: 1,\n    defaultMouth: 0,\n    sadMouth: 1,\n    cheekOpacity: 0.45,\n    spectacles: 0,\n  },\n  love: {\n    leftEyeStar: 1,\n    rightEyeStar: 1,\n    leftWantTwinkle: 1,\n    rightWantTwinkle: 1,\n    happyTwinkle: 1,\n    emotionFx: 1,\n    cheekOpacity: 0.95,\n  },\n  winky: {\n    leftDefaultEye: 0,\n    rightDefaultEye: 1,\n    leftWink: 1,\n    happyTwinkle: 0.8,\n    cheekOpacity: 0.75,\n    spectacles: 0,\n  },\n  surprised: {\n    defaultMouth: 0,\n    mouthOh: 1,\n  },\n  thinking: {\n    hmmLids: 1,\n    defaultMouth: 0.6,\n  },\n  \"side-eye\": {\n    leftDefaultEye: 0,\n    rightDefaultEye: 0,\n    sideEye: 1,\n    defaultMouth: 0.7,\n  },\n  shy: {\n    leftDefaultEye: 0,\n    rightDefaultEye: 0,\n    defaultMouth: 0,\n    passwordFace: 0,\n    shyFace: 1,\n    spectacles: 0,\n    cheekOpacity: 1,\n  },\n}\n\nexport function getRabbitMoodLayers(mood: RabbitMood): RabbitMoodLayers {\n  return { ...NEUTRAL_LAYERS, ...MOOD_LAYER_MAP[mood] }\n}\n\nfunction setOpacity(el: Element | null, opacity: number) {\n  if (!el) return\n  el.setAttribute(\"opacity\", String(opacity))\n}\n\nfunction setStyleTransform(el: Element | null, transform: string) {\n  if (!(el instanceof SVGElement)) return\n  el.style.transform = transform\n  el.style.transformBox = \"fill-box\"\n  el.style.transformOrigin = \"50% 50%\"\n}\n\nfunction setTransition(el: Element | null) {\n  if (!(el instanceof SVGElement)) return\n  el.style.transition =\n    \"opacity 0.28s ease, transform 0.28s ease, stroke-opacity 0.28s ease\"\n}\n\nexport function applyRabbitMood(root: ParentNode | null, mood: RabbitMood) {\n  if (!root) return\n\n  const layers = getRabbitMoodLayers(mood)\n\n  const leftEye = root.querySelector(\"#left-eye\")\n  const rightEye = root.querySelector(\"#right-eye\")\n  const leftEyeContent = leftEye?.querySelector(\":scope > g > g > g\")\n  const rightEyeContent = rightEye?.querySelector(\":scope > g > g > g\")\n\n  if (leftEyeContent instanceof SVGElement) {\n    setTransition(leftEyeContent)\n    setOpacity(leftEyeContent, layers.leftDefaultEye)\n  }\n\n  if (rightEyeContent instanceof SVGElement) {\n    setTransition(rightEyeContent)\n    setOpacity(rightEyeContent, layers.rightDefaultEye)\n  }\n\n  const targets: [string, number][] = [\n    [\"#password-face\", layers.passwordFace],\n    [\"#shy-face\", layers.shyFace],\n    [\"#side-eye-eyes\", layers.sideEye],\n    [\"#hmm-lids\", layers.hmmLids],\n    [\"#sad-brows\", layers.sadBrows],\n    [\"#happy-open-mouth\", layers.happyOpenMouth],\n    [\"#mouth\", layers.defaultMouth],\n    [\"#mouth-sad\", layers.sadMouth],\n    [\"#mouth-oh\", layers.mouthOh],\n    [\"#emotion-fx\", layers.emotionFx],\n    [\"#sad-tears\", layers.sadTears],\n    [\"#left-wink\", layers.leftWink],\n    [\"#right-wink\", layers.rightWink],\n    [\"#left-cheek\", layers.cheekOpacity],\n    [\"#right-cheek\", layers.cheekOpacity],\n    [\"#spectacles\", layers.spectacles],\n  ]\n\n  for (const [selector, opacity] of targets) {\n    const el = root.querySelector(selector)\n    setTransition(el)\n    setOpacity(el, opacity)\n  }\n\n  const sadTearPaths = root.querySelectorAll(\"#sad-tears path\")\n  sadTearPaths.forEach((path) => {\n    setTransition(path)\n    setOpacity(path, layers.sadTears)\n  })\n\n  const emotionSparkles = root.querySelector(\"#emotion-fx > g:not(#sad-tears)\")\n  if (emotionSparkles) {\n    setTransition(emotionSparkles)\n    setOpacity(emotionSparkles, layers.emotionFx)\n  }\n\n  const arcTargets: [string, number][] = [\n    [\"#left-eye-happy-arc\", layers.leftHappyArc],\n    [\"#right-eye-happy-arc\", layers.rightHappyArc],\n  ]\n\n  for (const [selector, active] of arcTargets) {\n    const el = root.querySelector(selector)\n    setTransition(el)\n    setOpacity(el, active)\n    setStyleTransform(el, active ? \"scaleY(1)\" : \"scaleY(0.4)\")\n  }\n\n  const starTargets: [string, number][] = [\n    [\"#left-eye-star\", layers.leftEyeStar],\n    [\"#right-eye-star\", layers.rightEyeStar],\n    [\"#left-eye-want-twinkle\", layers.leftWantTwinkle],\n    [\"#right-eye-want-twinkle\", layers.rightWantTwinkle],\n    [\"#happy-twinkle\", layers.happyTwinkle],\n  ]\n\n  for (const [selector, active] of starTargets) {\n    const el = root.querySelector(selector)\n    setTransition(el)\n    setOpacity(el, active)\n    setStyleTransform(el, active ? \"scale(1)\" : \"scale(0.5)\")\n  }\n\n  setStyleTransform(\n    root.querySelector(\"#mouth-oh\"),\n    layers.mouthOh ? \"scale(1)\" : \"scaleX(0.72) scaleY(0.35)\"\n  )\n\n  const ariaLabel = `Cute rabbit boy mascot, ${RABBIT_MOOD_LABELS[mood].toLowerCase()}`\n  const svg = root instanceof SVGSVGElement ? root : root.querySelector(\"svg\")\n  svg?.setAttribute(\"aria-label\", ariaLabel)\n\n  setOpacity(root.querySelector(\"#blink-lids\"), 0)\n  setOpacity(root.querySelector(\"#left-blink-lid\"), 0)\n  setOpacity(root.querySelector(\"#right-blink-lid\"), 0)\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/gammaui/rabbit-mascot-blink.ts",
      "content": "import {\n  getRabbitMoodLayers,\n  type RabbitMood,\n} from \"@/registry/gammaui/rabbit-mascot-moods\"\n\nconst BLINK_CLOSE_MS = 140\nconst BLINK_MIN_INTERVAL_MS = 2600\nconst BLINK_MAX_INTERVAL_MS = 5200\n\ninterface BlinkEyeState {\n  left: boolean\n  right: boolean\n}\n\n/** Moods where both round eyes stay open (not arcs, winks, or alternate face art). */\nexport function supportsRabbitBlink(mood: RabbitMood): boolean {\n  const layers = getRabbitMoodLayers(mood)\n\n  return (\n    layers.leftDefaultEye === 1 &&\n    layers.rightDefaultEye === 1 &&\n    layers.leftHappyArc === 0 &&\n    layers.rightHappyArc === 0 &&\n    layers.shyFace === 0 &&\n    layers.sideEye === 0 &&\n    layers.passwordFace === 0 &&\n    layers.leftWink === 0 &&\n    layers.rightWink === 0\n  )\n}\n\nfunction getBlinkEyeState(mood: RabbitMood): BlinkEyeState {\n  if (!supportsRabbitBlink(mood)) return { left: false, right: false }\n  return { left: true, right: true }\n}\n\nfunction setBlinkLayerOpacity(el: Element | null | undefined, opacity: number) {\n  if (!el) return\n  el.setAttribute(\"opacity\", String(opacity))\n}\n\nexport function triggerRabbitBlink(root: ParentNode | null, mood: RabbitMood) {\n  if (!root || !supportsRabbitBlink(mood)) return\n\n  const { left, right } = getBlinkEyeState(mood)\n  if (!left && !right) return\n\n  const blinkLids = root.querySelector(\"#blink-lids\")\n  const leftBlinkLid = root.querySelector(\"#left-blink-lid\")\n  const rightBlinkLid = root.querySelector(\"#right-blink-lid\")\n  const leftEye = root.querySelector(\"#left-eye\")\n  const rightEye = root.querySelector(\"#right-eye\")\n  const leftEyeContent = leftEye?.querySelector(\":scope > g > g > g\")\n  const rightEyeContent = rightEye?.querySelector(\":scope > g > g > g\")\n  const leftEyeStar = root.querySelector(\"#left-eye-star\")\n  const rightEyeStar = root.querySelector(\"#right-eye-star\")\n  const leftWantTwinkle = root.querySelector(\"#left-eye-want-twinkle\")\n  const rightWantTwinkle = root.querySelector(\"#right-eye-want-twinkle\")\n\n  setBlinkLayerOpacity(blinkLids, 1)\n  setBlinkLayerOpacity(leftBlinkLid, 1)\n  setBlinkLayerOpacity(rightBlinkLid, 1)\n  setBlinkLayerOpacity(leftEyeContent, 0)\n  setBlinkLayerOpacity(rightEyeContent, 0)\n  setBlinkLayerOpacity(leftEyeStar, 0)\n  setBlinkLayerOpacity(rightEyeStar, 0)\n  setBlinkLayerOpacity(leftWantTwinkle, 0)\n  setBlinkLayerOpacity(rightWantTwinkle, 0)\n\n  window.setTimeout(() => {\n    const layers = getRabbitMoodLayers(mood)\n\n    setBlinkLayerOpacity(blinkLids, 0)\n    setBlinkLayerOpacity(leftBlinkLid, 0)\n    setBlinkLayerOpacity(rightBlinkLid, 0)\n    setBlinkLayerOpacity(leftEyeContent, layers.leftDefaultEye)\n    setBlinkLayerOpacity(rightEyeContent, layers.rightDefaultEye)\n    setBlinkLayerOpacity(leftEyeStar, layers.leftEyeStar)\n    setBlinkLayerOpacity(rightEyeStar, layers.rightEyeStar)\n    setBlinkLayerOpacity(leftWantTwinkle, layers.leftWantTwinkle)\n    setBlinkLayerOpacity(rightWantTwinkle, layers.rightWantTwinkle)\n  }, BLINK_CLOSE_MS)\n}\n\nfunction randomBlinkDelayMs() {\n  return (\n    BLINK_MIN_INTERVAL_MS +\n    Math.random() * (BLINK_MAX_INTERVAL_MS - BLINK_MIN_INTERVAL_MS)\n  )\n}\n\nexport function startRabbitBlinkLoop(\n  root: ParentNode | null,\n  getMood: () => RabbitMood\n) {\n  if (!root) return () => {}\n\n  let timeoutId = window.setTimeout(function scheduleBlink() {\n    const mood = getMood()\n    if (supportsRabbitBlink(mood)) triggerRabbitBlink(root, mood)\n    timeoutId = window.setTimeout(scheduleBlink, randomBlinkDelayMs())\n  }, randomBlinkDelayMs())\n\n  return () => window.clearTimeout(timeoutId)\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/gammaui/rabbit-mascot-arm.ts",
      "content": "function setOpacity(el: Element | null, opacity: number) {\n  if (!el) return\n  el.setAttribute(\"opacity\", String(opacity))\n}\n\nexport function applyRabbitArmReach(\n  root: ParentNode | null,\n  reaching: boolean\n) {\n  if (!root) return\n\n  const rightArm = root.querySelector(\"#right-arm\")\n  const reachArm = root.querySelector(\"#right-arm-reach\")\n\n  if (rightArm instanceof SVGElement) {\n    rightArm.style.transition = reaching ? \"none\" : \"opacity 0.2s ease\"\n  }\n\n  setOpacity(rightArm, reaching ? 0 : 1)\n  setOpacity(reachArm, 0)\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}