{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-note",
  "type": "registry:component",
  "title": "Voice Note",
  "description": "Voice note interaction for recording, previewing, and managing audio snippets.",
  "dependencies": [
    "lucide-react",
    "motion",
    "react-icons"
  ],
  "files": [
    {
      "path": "components/watermelon/voice-note.tsx",
      "type": "registry:component",
      "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { motion, AnimatePresence, type Transition, MotionConfig } from 'motion/react';\nimport { Mic, X, Play, Square } from 'lucide-react';\nimport { RiSendPlaneFill } from 'react-icons/ri';\nimport { FaCheck } from 'react-icons/fa6';\n\nexport const RecorderState = {\n  IDLE: 'IDLE',\n  RECORDING: 'RECORDING',\n  REVIEWING: 'REVIEWING',\n  PLAYING: 'PLAYING',\n} as const;\n\nexport type RecorderState = (typeof RecorderState)[keyof typeof RecorderState];\n\ninterface VoiceNoteRecorderProps {\n  onSend?: (data: { duration: number; blob: Blob | null }) => void;\n  onCancel?: () => void;\n  maxDuration?: number;\n}\n\nexport const VoiceNote: React.FC<VoiceNoteRecorderProps> = ({\n  onSend,\n  onCancel,\n  maxDuration = 4,\n}) => {\n  const [state, setState] = useState<RecorderState>(RecorderState.IDLE);\n  const [duration, setDuration] = useState(0);\n  const [playbackTime, setPlaybackTime] = useState(0);\n\n  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n  const playbackTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n  const spring: Transition = { type: 'spring', stiffness: 400, damping: 40 }\n\n  const startRecording = () => {\n    setState(RecorderState.RECORDING);\n    setDuration(0);\n    timerRef.current = setInterval(() => {\n      setDuration((prev) => {\n        if (prev >= maxDuration) {\n          stopRecording();\n          return prev;\n        }\n        return prev + 1;\n      });\n    }, 1000);\n  };\n\n  const stopRecording = () => {\n    if (timerRef.current) clearInterval(timerRef.current);\n    setState(RecorderState.REVIEWING);\n  };\n\n  const resetRecorder = () => {\n    if (timerRef.current) clearInterval(timerRef.current);\n    if (playbackTimerRef.current) clearInterval(playbackTimerRef.current);\n    setDuration(0);\n    setPlaybackTime(0);\n    setState(RecorderState.IDLE);\n  };\n\n  const cancelRecording = () => {\n    resetRecorder();\n    onCancel?.();\n  };\n\n  const startPlayback = () => {\n    setState(RecorderState.PLAYING);\n    setPlaybackTime(duration);\n    playbackTimerRef.current = setInterval(() => {\n      setPlaybackTime((prev) => {\n        if (prev <= 0) {\n          stopPlayback();\n          return 0;\n        }\n        return prev - 1;\n      });\n    }, 1000);\n  };\n\n  const stopPlayback = () => {\n    if (playbackTimerRef.current) clearInterval(playbackTimerRef.current);\n    setPlaybackTime(0);\n    setState(RecorderState.REVIEWING);\n  };\n\n  const handleSend = () => {\n    onSend?.({ duration, blob: null });\n    resetRecorder();\n  };\n\n  const [barHeights, setBarHeights] = useState<number[][]>([]);\n\n  useEffect(() => {\n    const heights = [...Array(6)].map(() => [\n      8 + Math.random() * 6,\n      18 + Math.random() * 10,\n      12 + Math.random() * 8,\n      24 + Math.random() * 12,\n      10 + Math.random() * 6,\n    ]);\n    requestAnimationFrame(() => setBarHeights(heights));\n  }, []);\n\n\n  useEffect(() => {\n    return () => {\n      if (timerRef.current) clearInterval(timerRef.current);\n      if (playbackTimerRef.current) clearInterval(playbackTimerRef.current);\n    };\n  }, []);\n\n  const actionBtnClass = `w-16 h-16 rounded-full border-[1.6px]  flex items-center justify-center shrink-0 transition-colors duration-300 bg-[#fefefe] dark:bg-neutral-900 border-[#E8E7EF] dark:border-white/5`;\n\n  return (\n    <div className=\"flex min-h-full w-full flex-col items-center justify-center space-y-12 bg-transparent p-8 transition-colors duration-500\">\n      <div className=\"flex items-center gap-3\">\n        <MotionConfig transition={spring}>\n          <AnimatePresence mode=\"popLayout\">\n            {state !== RecorderState.IDLE && (\n              <motion.button\n                key=\"cancel-btn\"\n                initial={{ opacity: 0, filter: 'blur(4px)', x: '95px' }}\n                animate={{ opacity: 1, filter: 'blur(0)', x: '0px' }}\n                exit={{ opacity: 1, filter: 'blur(4px)', x: '95px' }}\n\n                onClick={cancelRecording}\n                className={actionBtnClass}\n              >\n                <X size={28} className=\"text-slate-700 dark:text-neutral-100\" />\n              </motion.button>\n            )}\n          </AnimatePresence>\n\n          <motion.div\n            animate={{\n              width: state === RecorderState.IDLE ? '65px' : '110px',\n            }}\n\n            className={`relative z-20 flex items-center justify-center overflow-hidden transition-colors duration-300 ${state === RecorderState.IDLE ? 'h-16 w-16' : 'h-16 px-6'} rounded-full border-[1.6px] ${state === RecorderState.RECORDING\n              ? 'border-none bg-[#FEE5E4] dark:bg-[#441010]'\n              : 'border-[#E8E7EF] bg-[#fefefe] dark:border-[#2d2d33] dark:bg-[#1a1a1e]'\n              } `}\n            style={{\n              borderRadius: 32,\n            }}\n          >\n            <AnimatePresence mode=\"popLayout\">\n              {state === RecorderState.RECORDING && (\n                <motion.svg\n                  className=\"pointer-events-none absolute inset-0 h-full w-full\"\n                  initial={{ opacity: 0, filter: 'blur(8px)' }}\n                  animate={{ opacity: 1, filter: 'blur(0)' }}\n                  exit={{ opacity: 0, filter: 'blur(8px)' }}\n                >\n                  <motion.rect\n                    x=\"2\"\n                    y=\"2\"\n                    rx=\"30\"\n                    width=\"calc(100% - 4px)\"\n                    height=\"calc(100% - 4px)\"\n                    fill=\"none\"\n                    stroke=\"#ef4444\"\n                    strokeWidth=\"3\"\n                    pathLength={1}\n                    strokeDasharray=\"1\"\n                    strokeDashoffset=\"1\"\n                    strokeLinecap=\"round\"\n                    initial={{ strokeDashoffset: 1 }}\n                    animate={{ strokeDashoffset: 0 }}\n                    transition={{\n                      duration: maxDuration,\n                      ease: 'linear',\n                    }}\n                  />\n                </motion.svg>\n              )}\n            </AnimatePresence>\n\n            <AnimatePresence mode=\"popLayout\" initial={false}>\n              {state === RecorderState.IDLE && (\n                <motion.button\n                  key=\"mic-icon\"\n                  initial={{ opacity: 0, filter: 'blur(8px)' }}\n                  animate={{ opacity: 1, filter: 'blur(0)' }}\n                  exit={{ opacity: 0, filter: 'blur(8px)' }}\n                  onClick={startRecording}\n                  className=\"flex items-center justify-center\"\n                >\n                  <Mic\n                    size={28}\n                    className=\"text-slate-800 dark:text-neutral-100\"\n                  />\n                </motion.button>\n              )}\n\n              {state === RecorderState.RECORDING && (\n                <motion.div\n                  key=\"recording-ui\"\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  exit={{ opacity: 0 }}\n                  className=\"z-10 flex items-center gap-1.5\"\n                >\n                  {barHeights.map((heights, i) => (\n                    <motion.div\n                      key={i}\n                      animate={{ height: heights }}\n                      transition={{\n                        duration: 1,\n                        repeat: Infinity,\n                        ease: 'linear',\n                        delay: i * 0.08,\n                      }}\n                      style={{ originY: 1 }}\n                      className=\"w-1.5 rounded-full bg-[#FC3229]\"\n                    />\n                  ))}\n                </motion.div>\n              )}\n\n              {(state === RecorderState.REVIEWING ||\n                state === RecorderState.PLAYING) && (\n                  <motion.div\n                    key=\"review-ui\"\n                    initial={{ opacity: 0, y: 5 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    exit={{ opacity: 0, y: -5 }}\n                    className=\"z-10 flex items-center gap-2\"\n                  >\n                    <motion.button\n                      key={\n                        state === RecorderState.PLAYING ? 'stop-btn' : 'play-btn'\n                      }\n                      onClick={\n                        state === RecorderState.PLAYING\n                          ? stopPlayback\n                          : startPlayback\n                      }\n                      className={`flex h-10 w-10 items-center justify-center rounded-full transition-colors ${state === RecorderState.PLAYING\n                        ? 'bg-transparent text-red-500 dark:text-red-400'\n                        : 'text-slate-800 dark:text-neutral-100'\n                        } `}\n                      initial={{ opacity: 0, filter: 'blur(4px)', scale: 0.25 }}\n                      animate={{ opacity: 1, filter: 'blur(0)', scale: 1 }}\n                      exit={{ opacity: 0, filter: 'blur(4px)', scale: 0.25 }}\n                    >\n                      {state === RecorderState.PLAYING ? (\n                        <Square size={22} fill=\"currentColor\" />\n                      ) : (\n                        <Play size={22} fill=\"currentColor\" />\n                      )}\n                    </motion.button>\n                    <span className=\"flex items-center gap-0.5 justify-center text-[20px] font-bold text-[#282828] tabular-nums transition-colors dark:text-neutral-100\">\n                      <AnimatedNumber\n                        value={\n                          state === RecorderState.PLAYING\n                            ? playbackTime\n                            : duration\n                        }\n                      />\n                      <motion.span layout>s</motion.span>\n                    </span>\n                  </motion.div>\n                )}\n            </AnimatePresence>\n          </motion.div>\n\n          <AnimatePresence mode=\"popLayout\">\n            {(state === RecorderState.RECORDING ||\n              state === RecorderState.REVIEWING ||\n              state === RecorderState.PLAYING) && (\n                <motion.button\n                  initial={{ opacity: 0, filter: 'blur(4px)', x: -95 }}\n                  animate={{ opacity: 1, filter: 'blur(0)', x: 0 }}\n                  exit={{ opacity: 0, filter: 'blur(4px)', x: -95 }}\n                  className={actionBtnClass}\n                  onClick={\n                    state === RecorderState.RECORDING\n                      ? stopRecording\n                      : handleSend\n                  }\n                >\n                  <AnimatePresence mode=\"popLayout\">\n                    <motion.div\n                      key={\n                        state === RecorderState.RECORDING\n                          ? 'check-btn'\n                          : state === RecorderState.REVIEWING ||\n                            state === RecorderState.PLAYING\n                            ? 'send-btn'\n                            : 'cancel-btn'\n                      }\n                      initial={{ opacity: 0, filter: 'blur(4px)', scale: 0.25 }}\n                      animate={{ opacity: 1, filter: 'blur(0)', scale: 1 }}\n                      exit={{ opacity: 0, filter: 'blur(4px)', scale: 0.25 }}\n                    >\n                      {state === RecorderState.RECORDING && (\n                        <FaCheck\n                          size={26}\n                          className=\"text-slate-700 dark:text-neutral-100\"\n                        />\n                      )}\n\n                      {(state === RecorderState.REVIEWING ||\n                        state === RecorderState.PLAYING) && (\n                          <RiSendPlaneFill\n                            size={26}\n                            className=\"text-[#272727] dark:text-neutral-100\"\n                          />\n                        )}\n                    </motion.div>\n                  </AnimatePresence>\n                </motion.button>\n              )}\n          </AnimatePresence>\n        </MotionConfig>\n      </div>\n    </div>\n  );\n};\n\ninterface AnimatedNumberProps {\n  value: number;\n  className?: string;\n}\n\nconst digitVariants = {\n  initial: (dir: number) => ({\n    y: dir > 0 ? 8 : -8,\n    opacity: 0,\n    scale: 0.5,\n    z: 0,\n    filter: 'blur(2px)',\n  }),\n  animate: {\n    y: 0,\n    opacity: 1,\n    scale: 1,\n    z: 10,\n    filter: 'blur(0px)',\n  },\n  exit: (dir: number) => ({\n    y: dir > 0 ? -8 : 8,\n    opacity: 0,\n    scale: 0.5,\n    z: 0,\n    filter: 'blur(2px)',\n  }),\n};\n\nexport function AnimatedNumber({ value, className }: AnimatedNumberProps) {\n  const [direction, setDirection] = React.useState(0);\n  const prevValueRef = React.useRef(value);\n\n  React.useEffect(() => {\n    const prev = prevValueRef.current;\n    if (value > prev) setDirection(1);\n    else if (value < prev) setDirection(-1);\n    prevValueRef.current = value;\n  }, [value]);\n\n  const digits = value.toString().split('');\n\n  const [prevDigits, setPrevDigits] = React.useState<string[]>([]);\n  const [prevTicks, setPrevTicks] = React.useState<number[]>([]);\n\n  const len = digits.length;\n  const lenDiff = len - prevDigits.length;\n\n  const nextTicks = digits.map((digit, i) => {\n    const prevI = i - lenDiff;\n    const prevDigit = prevI >= 0 ? prevDigits[prevI] : undefined;\n    const prevTick = prevI >= 0 ? prevTicks[prevI] : 0;\n    return digit !== prevDigit ? (prevTick ?? 0) + 1 : (prevTick ?? 0);\n  });\n\n  if (prevDigits.join(\"\") !== digits.join(\"\")) {\n    setPrevTicks(nextTicks);\n    setPrevDigits(digits);\n  }\n\n  return (\n    <div\n      className={`relative flex items-center justify-center gap-1 tabular-nums ${className ?? ''}`}\n    >\n      {digits.map((digit, index) => (\n        <div key={`${index}-${len}`} className=\"relative w-3\">\n          <AnimatePresence mode=\"popLayout\" initial={false} custom={direction}>\n            <motion.span\n              layout\n              key={nextTicks[index]}\n              custom={direction}\n              variants={digitVariants}\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              transition={{\n                type: 'spring',\n                stiffness: 200,\n                damping: 16,\n                mass: 1.2,\n              }}\n              className=\"absolute inset-0 flex items-center justify-center\"\n            >\n              {digit}\n            </motion.span>\n          </AnimatePresence>\n        </div>\n      ))}\n    </div>\n  );\n}\n"
    }
  ]
}
