{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-note-base",
  "type": "registry:component",
  "title": "Voice Note (base)",
  "description": "Theme-ready base variant of 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": "'use client';\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport {\n  motion,\n  AnimatePresence,\n  type Transition,\n  MotionConfig,\n} 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  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-lg border flex items-center justify-center shrink-0 transition-colors duration-300 bg-background border-border`;\n\n  return (\n    <div className=\"theme-injected 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                onClick={cancelRecording}\n                className={actionBtnClass}\n              >\n                <X size={28} className=\"text-muted-foreground\" />\n              </motion.button>\n            )}\n          </AnimatePresence>\n\n          <motion.div\n            animate={{\n              width: state === RecorderState.IDLE ? '65px' : '110px',\n            }}\n            className={`relative z-20 rounded-lg flex items-center justify-center overflow-hidden transition-colors duration-300 ${\n              state === RecorderState.IDLE ? 'h-16 w-16' : 'h-16 px-6'\n            } rounded-lg border ${\n              state === RecorderState.RECORDING\n                ? 'bg-destructive/10 border-none'\n                : 'border-border bg-background'\n            }`}\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=\"var(--radius)\"\n                    width=\"calc(100% - 4px)\"\n                    height=\"calc(100% - 4px)\"\n                    fill=\"none\"\n                    className=\"stroke-destructive\"\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 size={28} className=\"text-foreground\" />\n                </motion.button>\n              )}\n\n              {state === RecorderState.RECORDING && (\n                <motion.div\n                  key=\"recording-ui\"\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=\"bg-destructive w-1.5 rounded-lg\"\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                    onClick={\n                      state === RecorderState.PLAYING\n                        ? stopPlayback\n                        : startPlayback\n                    }\n                    className={`flex h-10 w-10 items-center justify-center rounded-lg transition-colors ${\n                      state === RecorderState.PLAYING\n                        ? 'text-destructive'\n                        : 'text-foreground'\n                    }`}\n                  >\n                    {state === RecorderState.PLAYING ? (\n                      <Square size={22} fill=\"currentColor\" />\n                    ) : (\n                      <Play size={22} fill=\"currentColor\" />\n                    )}\n                  </motion.button>\n\n                  <span className=\"text-foreground flex items-center justify-center gap-0.5 text-[20px] font-bold tabular-nums transition-colors\">\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                    {state === RecorderState.RECORDING && (\n                      <FaCheck size={26} className=\"text-muted-foreground\" />\n                    )}\n\n                    {(state === RecorderState.REVIEWING ||\n                      state === RecorderState.PLAYING) && (\n                      <RiSendPlaneFill size={26} className=\"text-foreground\" />\n                    )}\n                  </motion.div>\n                </AnimatePresence>\n              </motion.button>\n            )}\n          </AnimatePresence>\n        </MotionConfig>\n      </div>\n    </div>\n  );\n};\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"
    }
  ]
}
