{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "centered-feedback-drawer",
  "type": "registry:ui",
  "title": "Centered Feedback Drawer",
  "description": "Centered feedback panel — three SVG faces, contextual comment, spring selection, auto-dismiss thank-you state.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/centered-feedback-drawer.tsx",
      "content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\n/**\n * Centered Feedback Drawer — three emoji, one question.\n *\n * Not a form. Not a survey. Three faces —\n * 🙁 😐 😊 — universally understood in an instant.\n * Click one. It springs up, the others dim. A contextual\n * textarea slides in: \"What went wrong?\" or \"What did you\n * enjoy?\" — the interface reads your mood. Submit. Done.\n *\n * The faces are the interface.\n */\n\n/* ── Types ── */\n\nexport interface FeedbackData {\n  rating: number;\n  label: string;\n  message: string;\n}\n\nexport interface CenteredFeedbackDrawerProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSubmit?: (feedback: FeedbackData) => void;\n  sound?: boolean;\n}\n\n/* ── Constants ── */\n\nconst RATINGS = [\n  { label: \"Not great\", placeholder: \"What went wrong?\" },\n  { label: \"Okay\", placeholder: \"What could be better?\" },\n  { label: \"Great\", placeholder: \"What did you enjoy?\" },\n];\n\n/* ── Audio ── */\n\nlet _ctx: AudioContext | null = null;\nlet _buf: AudioBuffer | null = null;\n\nfunction audioCtx() {\n  if (!_ctx) {\n    _ctx = new (window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext })\n        .webkitAudioContext)();\n  }\n  if (_ctx.state === \"suspended\") _ctx.resume();\n  return _ctx;\n}\n\nfunction ensureBuf(ac: AudioContext): AudioBuffer {\n  if (_buf && _buf.sampleRate === ac.sampleRate) return _buf;\n  const rate = ac.sampleRate;\n  const len = Math.floor(rate * 0.003);\n  const buf = ac.createBuffer(1, len, rate);\n  const ch = buf.getChannelData(0);\n  for (let i = 0; i < len; i++) {\n    const t = i / len;\n    ch[i] = (Math.random() * 2 - 1) * (1 - t) ** 4;\n  }\n  _buf = buf;\n  return buf;\n}\n\nfunction playTick(last: React.MutableRefObject<number>) {\n  const now = performance.now();\n  if (now - last.current < 80) return;\n  last.current = now;\n  try {\n    const ac = audioCtx();\n    const buf = ensureBuf(ac);\n    const src = ac.createBufferSource();\n    const gain = ac.createGain();\n    src.buffer = buf;\n    src.playbackRate.value = 1.15;\n    gain.gain.value = 0.03;\n    src.connect(gain);\n    gain.connect(ac.destination);\n    src.start();\n  } catch {\n    /* silent */\n  }\n}\n\n/* ── Emoji Faces ── */\n\nconst EMOJIS = [\"🙁\", \"😐\", \"😊\"];\n\n/* ── Component ── */\n\nexport function CenteredFeedbackDrawer({\n  open,\n  onOpenChange,\n  onSubmit,\n  sound = true,\n}: CenteredFeedbackDrawerProps) {\n  const [rating, setRating] = useState<number | null>(null);\n  const [hovered, setHovered] = useState<number | null>(null);\n  const [message, setMessage] = useState(\"\");\n  const [submitted, setSubmitted] = useState(false);\n  const lastSound = useRef(0);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n  function reset() {\n    setRating(null);\n    setHovered(null);\n    setMessage(\"\");\n    setSubmitted(false);\n  }\n\n  function handleRate(i: number) {\n    setRating(i);\n    if (sound) playTick(lastSound);\n    setTimeout(() => textareaRef.current?.focus(), 220);\n  }\n\n  function handleSubmit() {\n    if (rating === null) return;\n    if (sound) playTick(lastSound);\n    onSubmit?.({\n      rating: rating + 1,\n      label: RATINGS[rating].label,\n      message,\n    });\n    setSubmitted(true);\n    setTimeout(() => {\n      onOpenChange(false);\n      setTimeout(reset, 300);\n    }, 1200);\n  }\n\n  function handleClose() {\n    if (submitted) return;\n    onOpenChange(false);\n    setTimeout(reset, 300);\n  }\n\n  /* ── Style helpers ── */\n\n  function faceOpacity(i: number): number {\n    const sel = rating === i;\n    const hov = hovered === i;\n    const has = rating !== null;\n\n    if (sel) return 1;\n    if (hov) return has ? 0.5 : 0.85;\n    return has ? 0.2 : 0.6;\n  }\n\n  function faceBg(i: number): string {\n    if (rating === i) return \"rgba(var(--d-ink),0.08)\";\n    if (hovered === i) return \"rgba(var(--d-ink),0.04)\";\n    return \"transparent\";\n  }\n\n  function faceBorder(i: number): string {\n    if (rating === i) return \"1px solid rgba(var(--d-ink),0.12)\";\n    return \"1px solid transparent\";\n  }\n\n  return (\n    <>\n      <style>{`\n        .cfd{--d-ink:0,0,0;--d-bg:rgba(255,255,255,0.98)}\n        .dark .cfd,[data-theme=\"dark\"] .cfd{--d-ink:255,255,255;--d-bg:rgba(24,24,26,0.98)}\n        .cfd textarea::placeholder{color:rgba(var(--d-ink),0.2)}\n      `}</style>\n      <AnimatePresence>\n        {open && (\n          <>\n            {/* Backdrop */}\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.2 }}\n              onClick={handleClose}\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                background: \"rgba(0,0,0,0.35)\",\n                zIndex: 40,\n              }}\n            />\n\n            {/* Centering wrapper */}\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                zIndex: 50,\n                pointerEvents: \"none\",\n              }}\n            >\n              {/* Panel */}\n              <motion.div\n                className=\"cfd\"\n                initial={{ opacity: 0, y: 8 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={{ opacity: 0, y: 8 }}\n                transition={{ type: \"spring\", damping: 28, stiffness: 350 }}\n                style={{\n                  pointerEvents: \"auto\",\n                  width: \"calc(100% - 32px)\",\n                  maxWidth: 320,\n                  background: \"var(--d-bg)\",\n                  border: \"1px solid rgba(var(--d-ink),0.06)\",\n                  borderRadius: 16,\n                  padding: \"28px 24px\",\n                }}\n              >\n                <AnimatePresence mode=\"wait\">\n                  {!submitted ? (\n                    <motion.div\n                      key=\"form\"\n                      exit={{ opacity: 0, y: -4 }}\n                      transition={{ duration: 0.1 }}\n                      style={{\n                        display: \"flex\",\n                        flexDirection: \"column\",\n                        alignItems: \"center\",\n                      }}\n                    >\n                      {/* Question */}\n                      <div\n                        style={{\n                          fontSize: 14,\n                          fontWeight: 500,\n                          letterSpacing: \"-0.01em\",\n                          color: \"rgba(var(--d-ink),0.5)\",\n                          marginBottom: 22,\n                        }}\n                      >\n                        How was your experience?\n                      </div>\n\n                      {/* Faces */}\n                      <div style={{ display: \"flex\", gap: 14 }}>\n                        {EMOJIS.map((emoji, i) => (\n                          <motion.button\n                            key={i}\n                            onClick={() => handleRate(i)}\n                            onMouseEnter={() => setHovered(i)}\n                            onMouseLeave={() => setHovered(null)}\n                            whileTap={{ scale: 0.92 }}\n                            animate={{ scale: rating === i ? 1.1 : 1 }}\n                            transition={{\n                              type: \"spring\",\n                              damping: 18,\n                              stiffness: 300,\n                            }}\n                            style={{\n                              width: 52,\n                              height: 52,\n                              borderRadius: 14,\n                              display: \"flex\",\n                              alignItems: \"center\",\n                              justifyContent: \"center\",\n                              cursor: \"pointer\",\n                              fontSize: 26,\n                              lineHeight: 1,\n                              opacity: faceOpacity(i),\n                              background: faceBg(i),\n                              border: faceBorder(i),\n                              transition:\n                                \"opacity 0.15s, background 0.15s, border 0.15s\",\n                            }}\n                          >\n                            {emoji}\n                          </motion.button>\n                        ))}\n                      </div>\n\n                      {/* Label — appears below faces after selection */}\n                      <div style={{ minHeight: 26, marginTop: 8 }}>\n                        <AnimatePresence mode=\"wait\">\n                          {rating !== null && (\n                            <motion.div\n                              key={rating}\n                              initial={{ opacity: 0, y: -4 }}\n                              animate={{ opacity: 1, y: 0 }}\n                              exit={{ opacity: 0 }}\n                              transition={{ duration: 0.12 }}\n                              style={{\n                                fontSize: 13,\n                                fontWeight: 500,\n                                letterSpacing: \"-0.01em\",\n                                color: \"rgba(var(--d-ink),0.4)\",\n                              }}\n                            >\n                              {RATINGS[rating].label}\n                            </motion.div>\n                          )}\n                        </AnimatePresence>\n                      </div>\n\n                      {/* Comment area — slides in after rating */}\n                      <AnimatePresence>\n                        {rating !== null && (\n                          <motion.div\n                            initial={{ opacity: 0, height: 0 }}\n                            animate={{ opacity: 1, height: \"auto\" }}\n                            exit={{ opacity: 0, height: 0 }}\n                            transition={{\n                              type: \"spring\",\n                              damping: 25,\n                              stiffness: 300,\n                            }}\n                            style={{\n                              width: \"100%\",\n                              overflow: \"hidden\",\n                            }}\n                          >\n                            {/* Hairline */}\n                            <div\n                              style={{\n                                height: 1,\n                                background: \"rgba(var(--d-ink),0.06)\",\n                                margin: \"6px 0 14px\",\n                              }}\n                            />\n\n                            {/* Textarea — contextual placeholder */}\n                            <textarea\n                              ref={textareaRef}\n                              value={message}\n                              onChange={(e) => setMessage(e.target.value)}\n                              placeholder={\n                                rating !== null\n                                  ? RATINGS[rating].placeholder\n                                  : \"\"\n                              }\n                              onKeyDown={(e) => {\n                                if (e.key === \"Enter\" && !e.shiftKey) {\n                                  e.preventDefault();\n                                  handleSubmit();\n                                }\n                              }}\n                              style={{\n                                width: \"100%\",\n                                minHeight: 60,\n                                resize: \"none\",\n                                background: \"transparent\",\n                                border: \"none\",\n                                outline: \"none\",\n                                fontSize: 13,\n                                fontWeight: 400,\n                                lineHeight: 1.6,\n                                color: \"rgba(var(--d-ink),0.65)\",\n                                fontFamily: \"inherit\",\n                              }}\n                            />\n\n                            {/* Send */}\n                            <div\n                              style={{\n                                display: \"flex\",\n                                justifyContent: \"flex-end\",\n                                paddingTop: 2,\n                              }}\n                            >\n                              <button\n                                onClick={handleSubmit}\n                                style={{\n                                  fontSize: 13,\n                                  fontWeight: 500,\n                                  color: message\n                                    ? \"rgba(var(--d-ink),0.6)\"\n                                    : \"rgba(var(--d-ink),0.3)\",\n                                  cursor: \"pointer\",\n                                  background: \"transparent\",\n                                  border: \"none\",\n                                  padding: \"4px 0\",\n                                  transition: \"color 0.15s\",\n                                }}\n                                onMouseEnter={(e) => {\n                                  e.currentTarget.style.color =\n                                    \"rgba(var(--d-ink),0.85)\";\n                                }}\n                                onMouseLeave={(e) => {\n                                  e.currentTarget.style.color = message\n                                    ? \"rgba(var(--d-ink),0.6)\"\n                                    : \"rgba(var(--d-ink),0.3)\";\n                                }}\n                              >\n                                Send\n                              </button>\n                            </div>\n                          </motion.div>\n                        )}\n                      </AnimatePresence>\n                    </motion.div>\n                  ) : (\n                    /* Thank you */\n                    <motion.div\n                      key=\"thanks\"\n                      initial={{ opacity: 0, y: 4 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      transition={{ duration: 0.2, delay: 0.05 }}\n                      style={{\n                        fontSize: 14,\n                        fontWeight: 500,\n                        letterSpacing: \"-0.01em\",\n                        color: \"rgba(var(--d-ink),0.5)\",\n                        padding: \"8px 0\",\n                        textAlign: \"center\",\n                      }}\n                    >\n                      Thank you.\n                    </motion.div>\n                  )}\n                </AnimatePresence>\n              </motion.div>\n            </div>\n          </>\n        )}\n      </AnimatePresence>\n    </>\n  );\n}\n\nexport default CenteredFeedbackDrawer;\n",
      "type": "registry:ui",
      "target": "components/ruixen/centered-feedback-drawer.tsx"
    }
  ]
}