{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-composer",
  "type": "registry:ui",
  "title": "Prompt Composer",
  "description": "A Tailwind prompt field with a sculpted arc of tinted action cards, a shared model-menu highlight and coordinated GSAP springs.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/prompt-composer-motion.ts",
      "content": "import { calcGeneratorDuration, spring } from \"motion\";\n\nexport const promptComposerBounded = (\n  value: number,\n  fallback: number,\n  min: number,\n  max: number,\n) => (Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback);\n\nexport function promptComposerSpring(\n  stiffness: number,\n  damping: number,\n  speed: number,\n  reduced: boolean | null,\n) {\n  const response = spring({\n    keyframes: [0, 1],\n    stiffness: promptComposerBounded(stiffness, 360, 120, 600),\n    damping: promptComposerBounded(damping, 32, 18, 60),\n    restSpeed: 0.001,\n    restDelta: 0.001,\n  });\n  const settle = calcGeneratorDuration(response);\n  return {\n    duration: reduced\n      ? 0\n      : settle / 1000 / promptComposerBounded(speed, 1, 0.5, 2),\n    ease: (progress: number) =>\n      progress === 1 ? 1 : response.next(progress * settle).value,\n  };\n}\n",
      "type": "registry:ui",
      "target": "@ui/prompt-composer-motion.ts"
    },
    {
      "path": "src/registry/ui/prompt-composer.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n  type FormEvent,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport {\n  ArrowUp,\n  CalendarDays,\n  FileText,\n  ImagePlus,\n  LoaderCircle,\n  Mic,\n  Plus,\n  StickyNote,\n  X,\n} from \"lucide-react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  promptComposerBounded as bounded,\n  promptComposerSpring,\n} from \"./prompt-composer-motion\";\nimport { PromptComposerArt, type PromptActionId } from \"./prompt-composer-art\";\nimport {\n  PromptComposerModelPicker,\n  type PromptComposerModel,\n} from \"./prompt-composer-model\";\nimport {\n  PromptComposerRecorder,\n  type PromptTranscriber,\n} from \"./prompt-composer-recorder\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type { PromptActionId, PromptComposerModel, PromptTranscriber };\n\nexport interface PromptComposerAction {\n  id: PromptActionId;\n  label: string;\n  placeholder: string;\n}\n\nexport interface PromptComposerSubmission {\n  text: string;\n  action: PromptActionId | null;\n  model: string;\n}\n\nexport interface PromptComposerProps {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  onSend?: (prompt: PromptComposerSubmission) => void | Promise<void>;\n  actions?: readonly PromptComposerAction[];\n  models?: readonly PromptComposerModel[];\n  defaultModel?: string;\n  onModelChange?: (model: PromptComposerModel) => void;\n  placeholder?: string;\n  label?: string;\n  theme?: \"system\" | \"light\" | \"dark\";\n  width?: number;\n  radius?: number;\n  cardLift?: number;\n  cardSpread?: number;\n  speed?: number;\n  stiffness?: number;\n  damping?: number;\n  accentColor?: string;\n  defaultTrayOpen?: boolean;\n  disabled?: boolean;\n  demoVoice?: boolean;\n  onTranscribe?: PromptTranscriber;\n  errorMessage?: string;\n  className?: string;\n  style?: CSSProperties;\n}\n\nexport const PROMPT_COMPOSER_ACTIONS: readonly PromptComposerAction[] = [\n  {\n    id: \"image\",\n    label: \"Create image\",\n    placeholder: \"Describe the image you want…\",\n  },\n  { id: \"note\", label: \"Add note\", placeholder: \"Write a note…\" },\n  {\n    id: \"event\",\n    label: \"Create event\",\n    placeholder: \"What’s the event, and when?\",\n  },\n  { id: \"voice\", label: \"Voice mode\", placeholder: \"\" },\n  { id: \"docs\", label: \"Analyse docs\", placeholder: \"Ask about a document…\" },\n];\n\nexport const PROMPT_COMPOSER_MODELS: readonly PromptComposerModel[] = [\n  { id: \"quick\", name: \"Quick 1.5\", description: \"Fast answers\" },\n  { id: \"deep\", name: \"Deep 2.0\", description: \"Thinks it through\" },\n  { id: \"vision\", name: \"Vision 1.2\", description: \"Sees your images\" },\n];\n\nconst cardStyle = {\n  image: { tilt: -8, rise: 0, layer: 1, Icon: ImagePlus },\n  note: { tilt: -4, rise: 7, layer: 2, Icon: StickyNote },\n  event: { tilt: 0, rise: 12, layer: 4, Icon: CalendarDays },\n  voice: { tilt: 4, rise: 7, layer: 3, Icon: Mic },\n  docs: { tilt: 8, rise: 0, layer: 2, Icon: FileText },\n} as const;\n\nexport const PromptComposer = forwardRef<HTMLDivElement, PromptComposerProps>(\n  function PromptComposer(\n    {\n      value,\n      defaultValue = \"\",\n      onValueChange,\n      onSend,\n      actions = PROMPT_COMPOSER_ACTIONS,\n      models = PROMPT_COMPOSER_MODELS,\n      defaultModel,\n      onModelChange,\n      placeholder = \"Ask anything\",\n      label = \"Message\",\n      theme = \"system\",\n      width = 760,\n      radius = 18,\n      cardLift = 16,\n      cardSpread = 10,\n      speed = 1,\n      stiffness = 360,\n      damping = 32,\n      accentColor = \"#8662d9\",\n      defaultTrayOpen = true,\n      disabled = false,\n      demoVoice = false,\n      onTranscribe,\n      errorMessage = \"Could not send your message. Please try again.\",\n      className,\n      style,\n    },\n    forwardedRef,\n  ) {\n    const rootRef = useRef<HTMLDivElement>(null);\n    const inputRef = useRef<HTMLInputElement>(null);\n    const mountedRef = useRef(false);\n    const sendingRef = useRef(false);\n    const wasSendingRef = useRef(false);\n    const timelineRef = useRef<gsap.core.Timeline | null>(null);\n    const [draft, setDraft] = useState(defaultValue);\n    const [actionId, setActionId] = useState<PromptActionId | null>(null);\n    const [trayOpen, setTrayOpen] = useState(defaultTrayOpen);\n    const [hovered, setHovered] = useState<PromptActionId | null>(null);\n    const [modelId, setModelId] = useState(defaultModel ?? models[0]?.id);\n    const [voiceOpen, setVoiceOpen] = useState(false);\n    const voiceWasOpenRef = useRef(false);\n    const [sending, setSending] = useState(false);\n    const [error, setError] = useState(\"\");\n    const [round, setRound] = useState(0);\n    const reduced = useReducedMotion();\n    const inputId = useId();\n    const fanId = useId();\n    const errorId = useId();\n    const text = value ?? draft;\n    const uniqueActions = actions\n      .filter(\n        (action, index) =>\n          actions.findIndex((candidate) => candidate.id === action.id) ===\n          index,\n      )\n      .slice(0, 5);\n    const availableModels = models.filter(\n      (model, index) =>\n        models.findIndex((candidate) => candidate.id === model.id) === index,\n    );\n    const model =\n      availableModels.find((candidate) => candidate.id === modelId) ??\n      availableModels[0];\n    const picked =\n      uniqueActions.find((action) => action.id === actionId) ?? null;\n    const voiceEnabled = Boolean(onTranscribe || demoVoice);\n    const [voiceAvailable, setVoiceAvailable] = useState(\n      !disabled && voiceEnabled,\n    );\n    if (voiceAvailable !== (!disabled && voiceEnabled)) {\n      setVoiceAvailable(!disabled && voiceEnabled);\n      setVoiceOpen(false);\n    }\n    const recording = voiceOpen && voiceAvailable;\n    const fanUp = trayOpen && !picked && !recording;\n    const busy = disabled || sending || recording;\n    const motionSpeed = bounded(speed, 1, 0.5, 2);\n    const canSend = Boolean(text.trim() && model && !busy);\n\n    useEffect(() => {\n      mountedRef.current = true;\n      return () => {\n        mountedRef.current = false;\n      };\n    }, []);\n\n    useEffect(() => {\n      if (voiceWasOpenRef.current && !recording && !disabled)\n        inputRef.current?.focus({ preventScroll: true });\n      voiceWasOpenRef.current = recording;\n    }, [recording, disabled]);\n\n    useEffect(() => {\n      if (wasSendingRef.current && !sending && !disabled)\n        inputRef.current?.focus({ preventScroll: true });\n      wasSendingRef.current = sending;\n    }, [sending, disabled]);\n\n    const updateText = (next: string) => {\n      setDraft(next);\n      onValueChange?.(next);\n    };\n    const focusInput = () => inputRef.current?.focus({ preventScroll: true });\n    const clearAction = () => {\n      setActionId(null);\n      setTrayOpen(true);\n      setHovered(null);\n      focusInput();\n    };\n\n    useGSAP(\n      () => {\n        gsap.set(\"[data-prompt-card]\", {\n          yPercent: 120,\n          autoAlpha: 0,\n          scale: 0.92,\n          rotation: 0,\n        });\n      },\n      { scope: rootRef },\n    );\n\n    useGSAP(\n      () => {\n        const root = rootRef.current;\n        if (!root) return;\n        const { duration, ease } = promptComposerSpring(\n          stiffness,\n          damping,\n          motionSpeed,\n          reduced,\n        );\n        timelineRef.current?.kill();\n        const timeline = gsap.timeline({\n          defaults: { duration, ease, overwrite: \"auto\" },\n        });\n        timelineRef.current = timeline;\n        const cards = Array.from(\n          root.querySelectorAll<HTMLButtonElement>(\"[data-prompt-card]\"),\n        );\n        const hoverIndex = cards.findIndex(\n          (card) => card.dataset.promptCard === hovered,\n        );\n        cards.forEach((card, index) => {\n          const id = uniqueActions[index]?.id;\n          if (!id) return;\n          const config = cardStyle[id];\n          const lifted = fanUp && hovered === id;\n          const near = hoverIndex >= 0 && Math.abs(hoverIndex - index) === 1;\n          const direction =\n            hoverIndex < 0 || lifted ? 0 : index < hoverIndex ? -1 : 1;\n          timeline.to(\n            card,\n            {\n              yPercent: fanUp ? 0 : 120,\n              y: fanUp\n                ? -config.rise -\n                  (lifted ? bounded(cardLift, 16, 0, 32) : near ? 3 : 0)\n                : 0,\n              x: fanUp\n                ? direction * bounded(cardSpread, 10, 0, 20) * (near ? 1 : 0.4)\n                : 0,\n              rotation: fanUp\n                ? lifted\n                  ? config.tilt * 0.3\n                  : config.tilt + direction * (near ? 1.5 : 0.5)\n                : 0,\n              scale: fanUp ? (lifted ? 1.05 : 1) : 0.92,\n              autoAlpha: fanUp ? 1 : 0,\n            },\n            reduced\n              ? 0\n              : hovered\n                ? 0\n                : fanUp\n                  ? index * 0.035\n                  : id === actionId\n                    ? 0.05\n                    : 0,\n          );\n          const artwork = (selector: string, vars: gsap.TweenVars) => {\n            const targets = card.querySelectorAll(selector);\n            if (targets.length)\n              timeline.to(\n                targets,\n                {\n                  transformOrigin: \"50% 50%\",\n                  duration,\n                  ...vars,\n                },\n                0,\n              );\n          };\n          artwork(\"[data-art-spark]\", {\n            rotation: lifted && !reduced ? 90 : 0,\n            scale: lifted && !reduced ? 1.2 : 1,\n          });\n          artwork(\"[data-art-triangle]\", {\n            rotation: lifted && !reduced ? -14 : 0,\n          });\n          artwork(\"[data-art-circle], [data-art-date], [data-art-doc]\", {\n            y: lifted && !reduced ? -3 : 0,\n          });\n          artwork(\"[data-art-line]\", {\n            scaleX: lifted && !reduced ? 0.9 : 1,\n            transformOrigin: \"0% 50%\",\n          });\n          artwork(\"[data-art-mic], [data-art-dot]\", {\n            scale: lifted && !reduced ? 1.08 : 1,\n          });\n          artwork(\"[data-art-wave]\", { scaleY: lifted && !reduced ? 0.65 : 1 });\n        });\n        timeline.to(\"[data-tray-icon]\", { rotation: fanUp ? 0 : 135 }, 0);\n        return () => {\n          timeline.kill();\n        };\n      },\n      {\n        scope: rootRef,\n        dependencies: [\n          fanUp,\n          hovered,\n          picked?.id,\n          voiceOpen,\n          reduced,\n          motionSpeed,\n          cardLift,\n          cardSpread,\n          stiffness,\n          damping,\n          round,\n          uniqueActions.map((action) => action.id).join(\",\"),\n        ],\n      },\n    );\n\n    useGSAP(\n      () => {\n        if (!voiceOpen)\n          gsap.fromTo(\n            \"[data-composer-content]\",\n            { opacity: 0, y: reduced ? 0 : 4 },\n            {\n              opacity: 1,\n              y: 0,\n              duration: reduced ? 0 : 0.2 / motionSpeed,\n              ease: \"power2.out\",\n              overwrite: true,\n            },\n          );\n      },\n      {\n        scope: rootRef,\n        dependencies: [picked?.id, voiceOpen, round, reduced, motionSpeed],\n      },\n    );\n\n    const send = async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n      if (!canSend || !model || sendingRef.current) return;\n      sendingRef.current = true;\n      setSending(true);\n      setError(\"\");\n      try {\n        await onSend?.({\n          text: text.trim(),\n          action: picked?.id ?? null,\n          model: model.id,\n        });\n        if (!mountedRef.current) return;\n        updateText(\"\");\n        setActionId(null);\n        setTrayOpen(true);\n        setHovered(null);\n        setRound((current) => current + 1);\n      } catch {\n        if (mountedRef.current) setError(errorMessage);\n      } finally {\n        sendingRef.current = false;\n        if (mountedRef.current) {\n          setSending(false);\n          inputRef.current?.focus({ preventScroll: true });\n        }\n      }\n    };\n\n    const tone =\n      \"[--pc-paper:#fff] [--pc-ink:#36333d] [--pc-muted:#77727f] [--pc-border:#3225431c] [--pc-pill:#f1eff5] [--pc-field:#fff] [--pc-image:#e7dcfa] [--pc-note:#f9edc8] [--pc-event:#d9eddf] [--pc-voice:#f6ded9] [--pc-docs:#dbe8f7]\";\n    const darkTone =\n      \"[--pc-paper:#25232a] [--pc-ink:#eeeaf4] [--pc-muted:#b1abbc] [--pc-border:#e8daff1c] [--pc-pill:#302c38] [--pc-field:#211f26] [--pc-image:#40334f] [--pc-note:#4c4430] [--pc-event:#2d463d] [--pc-voice:#503835] [--pc-docs:#304156]\";\n    const systemTone =\n      \"dark:[--pc-paper:#25232a] dark:[--pc-ink:#eeeaf4] dark:[--pc-muted:#b1abbc] dark:[--pc-border:#e8daff1c] dark:[--pc-pill:#302c38] dark:[--pc-field:#211f26] dark:[--pc-image:#40334f] dark:[--pc-note:#4c4430] dark:[--pc-event:#2d463d] dark:[--pc-voice:#503835] dark:[--pc-docs:#304156]\";\n    const ChipIcon = picked ? cardStyle[picked.id].Icon : null;\n\n    return (\n      <div\n        ref={(element) => {\n          rootRef.current = element;\n          if (typeof forwardedRef === \"function\") forwardedRef(element);\n          else if (forwardedRef) forwardedRef.current = element;\n        }}\n        data-slot=\"prompt-composer\"\n        data-theme={theme}\n        className={cn(\n          \"@container/composer w-full min-w-0 text-[var(--pc-ink)]\",\n          tone,\n          theme === \"dark\" ? darkTone : theme === \"system\" ? systemTone : \"\",\n          className,\n        )}\n        style={\n          {\n            maxWidth: bounded(width, 760, 280, 1200),\n            \"--pc-radius\": `${bounded(radius, 18, 10, 28)}px`,\n            \"--pc-accent\": accentColor,\n            ...style,\n          } as CSSProperties\n        }\n      >\n        <form\n          onSubmit={send}\n          aria-label=\"Prompt composer\"\n          aria-busy={sending}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\" && voiceOpen) {\n              setVoiceOpen(false);\n              focusInput();\n            }\n          }}\n          className=\"relative grid min-w-0 grid-cols-1 items-end gap-x-3 gap-y-3 @min-[561px]/composer:grid-cols-[auto_minmax(0,1fr)]\"\n        >\n          {model && (\n            <div className=\"order-2 @min-[561px]/composer:order-0 @min-[561px]/composer:pb-2\">\n              <PromptComposerModelPicker\n                models={availableModels}\n                value={model}\n                onChange={(next) => {\n                  setModelId(next.id);\n                  onModelChange?.(next);\n                }}\n                disabled={busy}\n                speed={motionSpeed}\n                stiffness={stiffness}\n                damping={damping}\n              />\n            </div>\n          )}\n          <div\n            className={cn(\n              \"@container/field min-w-0\",\n              !model && \"col-span-full\",\n            )}\n          >\n            <div\n              className={cn(\n                \"relative min-w-0\",\n                uniqueActions.length ? \"pt-[calc(16cqw+22px)]\" : \"pt-0\",\n              )}\n            >\n              <div\n                id={fanId}\n                inert={!fanUp || busy}\n                aria-hidden={!fanUp}\n                className=\"absolute inset-x-0 top-2 h-[22cqw] [clip-path:inset(-64px_-32px_calc(6cqw_-_42px)_-32px)]\"\n                onPointerLeave={() => setHovered(null)}\n              >\n                {uniqueActions.map((action, index) => {\n                  const config = cardStyle[action.id];\n                  const isVoiceDisabled =\n                    action.id === \"voice\" && !voiceEnabled;\n                  return (\n                    <button\n                      key={action.id}\n                      type=\"button\"\n                      data-prompt-card={action.id}\n                      aria-label={action.label}\n                      disabled={busy || isVoiceDisabled}\n                      tabIndex={fanUp && !busy ? 0 : -1}\n                      onPointerEnter={(event) => {\n                        if (event.pointerType === \"mouse\")\n                          setHovered(action.id);\n                      }}\n                      onFocus={() => setHovered(action.id)}\n                      onBlur={() => setHovered(null)}\n                      onClick={() => {\n                        setHovered(null);\n                        setError(\"\");\n                        if (action.id === \"voice\") setVoiceOpen(true);\n                        else {\n                          setActionId(action.id);\n                          focusInput();\n                        }\n                      }}\n                      className=\"absolute top-0 h-[22cqw] w-[17cqw] origin-bottom cursor-pointer rounded-[clamp(9px,2.3cqw,17px)] shadow-[0_2px_3px_#20102d08,0_12px_20px_-8px_#20102d20] outline-none focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)] disabled:cursor-default disabled:opacity-60\"\n                      style={\n                        {\n                          left: `calc(${50 + (index - (uniqueActions.length - 1) / 2) * 18.8}% - 8.5cqw)`,\n                          \"--pc-tint\": `var(--pc-${action.id})`,\n                          zIndex: hovered === action.id ? 10 : config.layer,\n                          transform: `rotate(${config.tilt}deg)`,\n                        } as CSSProperties\n                      }\n                    >\n                      <span className=\"absolute inset-0 overflow-hidden rounded-[inherit] border border-[var(--pc-border)] bg-[var(--pc-tint)] shadow-[inset_0_1px_0_#ffffff50]\">\n                        <span\n                          aria-hidden=\"true\"\n                          className=\"absolute inset-x-[10%] top-[9%] flex items-center justify-between text-[clamp(6px,1cqw,8px)] font-medium text-[var(--pc-ink)]/35\"\n                        >\n                          <span className=\"size-[0.65cqw] rounded-full bg-current\" />\n                          <span className=\"font-mono\">0{index + 1}</span>\n                        </span>\n                        <span className=\"absolute inset-x-[7%] top-[23%] text-center text-[clamp(8px,1.65cqw,12px)] leading-tight font-medium tracking-[-0.02em] text-[var(--pc-ink)]/75\">\n                          {action.label}\n                        </span>\n                        <span\n                          aria-hidden=\"true\"\n                          className=\"absolute inset-x-0 top-[43%] bottom-[2%] flex items-start justify-center text-[color-mix(in_srgb,var(--pc-ink)_45%,var(--pc-tint))]\"\n                        >\n                          <PromptComposerArt id={action.id} />\n                        </span>\n                      </span>\n                    </button>\n                  );\n                })}\n              </div>\n              <div className=\"relative z-20 flex h-14 min-w-0 items-center rounded-[var(--pc-radius)] border border-[var(--pc-border)] bg-[var(--pc-field)] pr-2 shadow-[0_2px_4px_#20102d05,0_12px_32px_-12px_#20102d25,inset_0_1px_0_#ffffff10] focus-within:ring-2 focus-within:ring-[var(--pc-accent)]/30\">\n                {recording ? (\n                  <PromptComposerRecorder\n                    demo={!onTranscribe && demoVoice}\n                    onTranscribe={onTranscribe}\n                    onComplete={(transcript) => {\n                      updateText(\n                        [text.trim(), transcript].filter(Boolean).join(\" \"),\n                      );\n                      setVoiceOpen(false);\n                      focusInput();\n                    }}\n                    onCancel={() => {\n                      setVoiceOpen(false);\n                      focusInput();\n                    }}\n                    onError={(message) => {\n                      setError(message);\n                      setVoiceOpen(false);\n                      focusInput();\n                    }}\n                  />\n                ) : (\n                  <div\n                    data-composer-content\n                    className=\"flex h-full min-w-0 flex-1 items-center gap-1 pl-2.5 @min-[421px]/field:gap-2 @min-[421px]/field:pl-4\"\n                  >\n                    {picked && ChipIcon && (\n                      <button\n                        type=\"button\"\n                        disabled={busy}\n                        onClick={clearAction}\n                        aria-label={`Remove ${picked.label}`}\n                        data-action-chip\n                        className=\"flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-lg bg-[var(--pc-pill)] px-2 text-[11px] font-medium outline-none focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)] disabled:opacity-50\"\n                      >\n                        <ChipIcon\n                          className=\"size-3.5 text-[var(--pc-accent)]\"\n                          aria-hidden=\"true\"\n                        />\n                        <span className=\"hidden max-w-28 truncate @min-[481px]/field:inline\">\n                          {picked.label}\n                        </span>\n                        <X className=\"size-3\" aria-hidden=\"true\" />\n                      </button>\n                    )}\n                    <label htmlFor={inputId} className=\"sr-only\">\n                      {label}\n                    </label>\n                    <input\n                      ref={inputRef}\n                      id={inputId}\n                      value={text}\n                      onChange={(event) => {\n                        updateText(event.target.value);\n                        if (error) setError(\"\");\n                      }}\n                      onKeyDown={(event) => {\n                        if (\n                          picked &&\n                          (event.key === \"Escape\" ||\n                            (event.key === \"Backspace\" && !text))\n                        ) {\n                          event.preventDefault();\n                          clearAction();\n                        }\n                      }}\n                      disabled={busy}\n                      placeholder={picked?.placeholder ?? placeholder}\n                      autoComplete=\"off\"\n                      aria-invalid={Boolean(error)}\n                      aria-describedby={error ? errorId : undefined}\n                      className=\"h-full w-0 min-w-0 flex-1 bg-transparent text-base text-[var(--pc-ink)] outline-none placeholder:text-[var(--pc-muted)] disabled:opacity-60 @min-[561px]/composer:text-sm\"\n                    />\n                    <button\n                      type=\"button\"\n                      aria-label={\n                        fanUp ? \"Hide quick actions\" : \"Show quick actions\"\n                      }\n                      aria-expanded={fanUp}\n                      aria-controls={fanId}\n                      disabled={busy}\n                      onClick={() => {\n                        if (picked) clearAction();\n                        else setTrayOpen(!trayOpen);\n                        setHovered(null);\n                      }}\n                      className=\"grid size-10 shrink-0 cursor-pointer place-items-center rounded-full text-[var(--pc-ink)] outline-none hover:bg-[var(--pc-pill)] focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)] disabled:opacity-40\"\n                    >\n                      <Plus\n                        data-tray-icon\n                        className=\"size-5\"\n                        aria-hidden=\"true\"\n                      />\n                    </button>\n                    <button\n                      type=\"button\"\n                      aria-label=\"Voice mode\"\n                      disabled={busy || !voiceEnabled}\n                      onClick={() => {\n                        setError(\"\");\n                        setHovered(null);\n                        setVoiceOpen(true);\n                      }}\n                      className=\"grid size-10 shrink-0 cursor-pointer place-items-center rounded-full outline-none hover:bg-[var(--pc-pill)] focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)] disabled:cursor-default disabled:opacity-35\"\n                    >\n                      <Mic className=\"size-4.5\" aria-hidden=\"true\" />\n                    </button>\n                    <button\n                      type=\"submit\"\n                      aria-label={sending ? \"Sending message\" : \"Send message\"}\n                      disabled={!canSend}\n                      className=\"grid size-9 shrink-0 cursor-pointer place-items-center rounded-xl bg-[var(--pc-accent)] text-white shadow-[inset_0_1px_0_#ffffff20] outline-none focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)] focus-visible:ring-offset-2 disabled:cursor-default disabled:bg-[var(--pc-pill)] disabled:text-[var(--pc-muted)] disabled:shadow-none\"\n                    >\n                      {sending ? (\n                        <LoaderCircle\n                          className=\"size-4 motion-safe:animate-spin\"\n                          aria-hidden=\"true\"\n                        />\n                      ) : (\n                        <ArrowUp\n                          className=\"size-4\"\n                          strokeWidth={2.5}\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                    </button>\n                  </div>\n                )}\n              </div>\n            </div>\n          </div>\n          {error && (\n            <p\n              id={errorId}\n              role=\"alert\"\n              className=\"col-span-full order-3 max-w-full text-xs leading-5 text-red-500\"\n            >\n              {error}\n            </p>\n          )}\n        </form>\n      </div>\n    );\n  },\n);\n\nexport default PromptComposer;\n",
      "type": "registry:ui",
      "target": "@ui/prompt-composer.tsx"
    },
    {
      "path": "src/registry/ui/prompt-composer-art.tsx",
      "content": "\"use client\";\n\nexport type PromptActionId = \"image\" | \"note\" | \"event\" | \"voice\" | \"docs\";\n\nexport function PromptComposerArt({ id }: { id: PromptActionId }) {\n  if (id === \"note\")\n    return (\n      <span className=\"relative mt-[1cqw] flex w-[72%] -rotate-3 flex-col gap-[1.5cqw] rounded-[1cqw] border border-current/15 bg-[var(--pc-paper)]/50 px-[1cqw] py-[1.5cqw] shadow-[2px_3px_0_#00000006]\">\n        {[0.7, 1, 0.9, 0.55].map((width, index) => (\n          <span\n            key={index}\n            data-art-line\n            className=\"h-[1.5px] origin-left rounded-full bg-current opacity-55\"\n            style={{ width: `${width * 100}%` }}\n          />\n        ))}\n        <span className=\"absolute -top-[0.7cqw] right-[1.2cqw] h-[1.4cqw] w-[4.5cqw] rotate-8 rounded-sm bg-current/15\" />\n      </span>\n    );\n\n  if (id === \"event\")\n    return (\n      <span\n        data-art-date\n        className=\"relative flex w-[66%] flex-col items-center rounded-[1cqw] border border-current/15 bg-[var(--pc-paper)]/55 pb-[1cqw] shadow-[2px_3px_0_#00000006]\"\n      >\n        <span className=\"w-full border-b border-current/10 py-[0.5cqw] text-center text-[1.3cqw] font-semibold tracking-[0.15em]\">\n          OCT\n        </span>\n        <span className=\"text-[6cqw] leading-[1.15] font-semibold tracking-[-0.08em]\">\n          12\n        </span>\n        <span className=\"absolute -top-[0.7cqw] left-[1.2cqw] h-[1.3cqw] w-[0.45cqw] rounded-full bg-current/50\" />\n        <span className=\"absolute -top-[0.7cqw] right-[1.2cqw] h-[1.3cqw] w-[0.45cqw] rounded-full bg-current/50\" />\n      </span>\n    );\n\n  if (id === \"voice\")\n    return (\n      <svg\n        className=\"w-[78%] overflow-visible\"\n        viewBox=\"0 0 96 80\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n      >\n        <rect\n          x=\"9\"\n          y=\"11\"\n          width=\"78\"\n          height=\"55\"\n          rx=\"15\"\n          fill=\"currentColor\"\n          fillOpacity=\"0.08\"\n          stroke=\"currentColor\"\n          strokeOpacity=\"0.12\"\n        />\n        {[14, 27, 39, 24, 47, 32, 18].map((height, index) => (\n          <rect\n            key={index}\n            data-art-wave\n            x={22 + index * 8}\n            y={39 - height / 2}\n            width=\"4\"\n            height={height}\n            rx=\"2\"\n            fill=\"currentColor\"\n            fillOpacity={index === 3 ? 0.9 : 0.45}\n          />\n        ))}\n        <circle data-art-dot cx=\"76\" cy=\"63\" r=\"9\" fill=\"var(--pc-paper)\" />\n        <circle cx=\"76\" cy=\"63\" r=\"4\" fill=\"currentColor\" />\n      </svg>\n    );\n\n  if (id === \"docs\")\n    return (\n      <svg\n        className=\"w-[74%] overflow-visible\"\n        viewBox=\"0 0 96 80\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n      >\n        <rect\n          x=\"20\"\n          y=\"9\"\n          width=\"49\"\n          height=\"60\"\n          rx=\"7\"\n          transform=\"rotate(-9 20 9)\"\n          fill=\"currentColor\"\n          fillOpacity=\"0.13\"\n        />\n        <g data-art-doc>\n          <rect\n            x=\"27\"\n            y=\"8\"\n            width=\"49\"\n            height=\"61\"\n            rx=\"7\"\n            fill=\"var(--pc-paper)\"\n            fillOpacity=\"0.65\"\n            stroke=\"currentColor\"\n            strokeOpacity=\"0.2\"\n          />\n          <rect\n            x=\"37\"\n            y=\"21\"\n            width=\"16\"\n            height=\"5\"\n            rx=\"2.5\"\n            fill=\"currentColor\"\n            fillOpacity=\"0.55\"\n          />\n          <path\n            d=\"M37 36h29M37 44h29M37 52h17\"\n            stroke=\"currentColor\"\n            strokeOpacity=\"0.35\"\n            strokeWidth=\"3\"\n            strokeLinecap=\"round\"\n          />\n        </g>\n        <circle cx=\"72\" cy=\"63\" r=\"11\" fill=\"currentColor\" fillOpacity=\"0.8\" />\n        <path\n          d=\"m67 63 3 3 6-7\"\n          stroke=\"var(--pc-paper)\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    );\n\n  return (\n    <svg\n      className=\"w-[80%] overflow-visible\"\n      viewBox=\"0 0 96 80\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <rect\n        x=\"13\"\n        y=\"12\"\n        width=\"65\"\n        height=\"50\"\n        rx=\"8\"\n        transform=\"rotate(-8 13 12)\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.12\"\n      />\n      <g data-art-triangle>\n        <rect\n          x=\"19\"\n          y=\"15\"\n          width=\"65\"\n          height=\"50\"\n          rx=\"8\"\n          fill=\"var(--pc-paper)\"\n          fillOpacity=\"0.65\"\n          stroke=\"currentColor\"\n          strokeOpacity=\"0.2\"\n        />\n        <path\n          d=\"m25 56 16-19 12 13 8-9 17 15H25Z\"\n          fill=\"currentColor\"\n          fillOpacity=\"0.3\"\n        />\n      </g>\n      <circle\n        data-art-circle\n        cx=\"66\"\n        cy=\"29\"\n        r=\"6\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.4\"\n      />\n      <path\n        data-art-spark\n        d=\"M81 2c1 7 3 9 10 10-7 1-9 3-10 10-1-7-3-9-10-10 7-1 9-3 10-10Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.7\"\n      />\n    </svg>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/prompt-composer-art.tsx"
    },
    {
      "path": "src/registry/ui/prompt-composer-model.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type KeyboardEvent,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { Check, ChevronDown, Flower } from \"lucide-react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { promptComposerSpring } from \"./prompt-composer-motion\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface PromptComposerModel {\n  id: string;\n  name: string;\n  description?: string;\n}\n\nexport function PromptComposerModelPicker({\n  models,\n  value,\n  onChange,\n  disabled,\n  speed,\n  stiffness,\n  damping,\n}: {\n  models: readonly PromptComposerModel[];\n  value: PromptComposerModel;\n  onChange: (model: PromptComposerModel) => void;\n  disabled: boolean;\n  speed: number;\n  stiffness: number;\n  damping: number;\n}) {\n  const [open, setOpen] = useState(false);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const menuRef = useRef<HTMLDivElement>(null);\n  const highlightRef = useRef<HTMLSpanElement>(null);\n  const pinnedRef = useRef(false);\n  const focusOnOpenRef = useRef(false);\n  const focusEdgeRef = useRef<\"first\" | \"last\" | \"selected\">(\"selected\");\n  const closeDelayRef = useRef<gsap.core.Tween | null>(null);\n  const highlightedRef = useRef(false);\n  const [hovered, setHovered] = useState<string | null>(null);\n  const menuId = useId();\n  const reduced = useReducedMotion();\n  const visible = open && !disabled;\n  const { contextSafe } = useGSAP(() => () => closeDelayRef.current?.kill(), {\n    scope: rootRef,\n  });\n\n  const close = useCallback(() => {\n    closeDelayRef.current?.kill();\n    focusOnOpenRef.current = false;\n    pinnedRef.current = false;\n    setOpen(false);\n    setHovered(null);\n  }, []);\n\n  const focusMenu = useCallback((edge: \"first\" | \"last\" | \"selected\") => {\n    const options = Array.from(\n      menuRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[role=\"menuitemradio\"]',\n      ) ?? [],\n    );\n    const option =\n      edge === \"first\"\n        ? options[0]\n        : edge === \"last\"\n          ? options.at(-1)\n          : options.find(\n              (item) => item.getAttribute(\"aria-checked\") === \"true\",\n            );\n    option?.focus({ preventScroll: true });\n  }, []);\n\n  const scheduleClose = () =>\n    contextSafe(() => {\n      if (\n        pinnedRef.current ||\n        menuRef.current?.contains(document.activeElement)\n      )\n        return;\n      closeDelayRef.current?.kill();\n      closeDelayRef.current = gsap.delayedCall(0.18, close);\n    })();\n\n  useGSAP(\n    () => {\n      gsap.to(menuRef.current, {\n        autoAlpha: visible ? 1 : 0,\n        y: visible ? 0 : 7,\n        scale: visible ? 1 : 0.96,\n        ...promptComposerSpring(stiffness, damping, speed, reduced),\n        overwrite: true,\n      });\n      gsap.to(\"[data-model-chevron]\", {\n        rotation: visible ? 180 : 0,\n        ...promptComposerSpring(stiffness, damping, speed, reduced),\n        overwrite: true,\n      });\n    },\n    {\n      scope: rootRef,\n      dependencies: [visible, reduced, speed, stiffness, damping],\n    },\n  );\n\n  useGSAP(\n    () => {\n      const target = Array.from(\n        menuRef.current?.querySelectorAll<HTMLButtonElement>(\n          \"[data-model-option]\",\n        ) ?? [],\n      ).find((option) => option.dataset.modelOption === (hovered ?? value.id));\n      if (!target || !visible) {\n        highlightedRef.current = false;\n        return;\n      }\n      const vars = {\n        y: target.offsetTop,\n        height: target.offsetHeight,\n        autoAlpha: 1,\n      };\n      if (!highlightedRef.current || reduced)\n        gsap.set(highlightRef.current, vars);\n      else\n        gsap.to(highlightRef.current, {\n          ...vars,\n          ...promptComposerSpring(stiffness, damping, speed, reduced),\n          overwrite: true,\n        });\n      highlightedRef.current = true;\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        hovered,\n        visible,\n        value.id,\n        reduced,\n        speed,\n        stiffness,\n        damping,\n        models.map((model) => model.id).join(\",\"),\n      ],\n    },\n  );\n\n  useEffect(() => {\n    if (!visible) return;\n    if (focusOnOpenRef.current) focusMenu(focusEdgeRef.current);\n    focusOnOpenRef.current = false;\n    const dismiss = (event: PointerEvent) => {\n      if (\n        event.target instanceof Node &&\n        !rootRef.current?.contains(event.target)\n      )\n        close();\n    };\n    document.addEventListener(\"pointerdown\", dismiss);\n    return () => document.removeEventListener(\"pointerdown\", dismiss);\n  }, [visible, close, focusMenu]);\n\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Escape\") {\n      event.preventDefault();\n      event.stopPropagation();\n      close();\n      triggerRef.current?.focus({ preventScroll: true });\n      return;\n    }\n    const options = Array.from(\n      menuRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[role=\"menuitemradio\"]',\n      ) ?? [],\n    );\n    const index = options.indexOf(document.activeElement as HTMLButtonElement);\n    const next =\n      event.key === \"Home\"\n        ? 0\n        : event.key === \"End\"\n          ? options.length - 1\n          : event.key === \"ArrowDown\"\n            ? (index + 1) % options.length\n            : event.key === \"ArrowUp\"\n              ? (index - 1 + options.length) % options.length\n              : null;\n    if (next !== null) {\n      event.preventDefault();\n      options[next]?.focus({ preventScroll: true });\n    }\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className=\"relative z-30 w-fit shrink-0\"\n      onPointerEnter={(event) => {\n        closeDelayRef.current?.kill();\n        if (event.pointerType === \"mouse\" && !disabled && models.length > 1)\n          setOpen(true);\n      }}\n      onPointerLeave={(event) => {\n        if (event.pointerType === \"mouse\") scheduleClose();\n      }}\n      onBlur={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget)) close();\n      }}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-label={`Model: ${value.name}`}\n        aria-haspopup=\"menu\"\n        aria-expanded={visible}\n        aria-controls={menuId}\n        disabled={disabled || models.length < 2}\n        onClick={() => {\n          closeDelayRef.current?.kill();\n          if (visible && pinnedRef.current) close();\n          else {\n            pinnedRef.current = true;\n            focusEdgeRef.current = \"selected\";\n            focusOnOpenRef.current = true;\n            setOpen(true);\n            if (visible) {\n              focusOnOpenRef.current = false;\n              focusMenu(\"selected\");\n            }\n          }\n        }}\n        onKeyDown={(event) => {\n          if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n            event.preventDefault();\n            closeDelayRef.current?.kill();\n            pinnedRef.current = true;\n            const edge = event.key === \"ArrowUp\" ? \"last\" : \"first\";\n            if (visible) focusMenu(edge);\n            else {\n              focusEdgeRef.current = edge;\n              focusOnOpenRef.current = true;\n              setOpen(true);\n            }\n          }\n        }}\n        className=\"flex min-h-10 w-36 max-w-full cursor-pointer items-center gap-2 rounded-full bg-[var(--pc-pill)] px-3.5 text-xs font-medium text-[var(--pc-muted)] outline-none hover:text-[var(--pc-ink)] focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)] focus-visible:ring-offset-2 disabled:cursor-default disabled:opacity-60\"\n      >\n        <Flower className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n        <span data-model-name className=\"truncate\">\n          {value.name}\n        </span>\n        <ChevronDown\n          data-model-chevron\n          className=\"size-3 shrink-0\"\n          aria-hidden=\"true\"\n        />\n      </button>\n      <div\n        ref={menuRef}\n        id={menuId}\n        role=\"menu\"\n        aria-label=\"Choose a model\"\n        inert={!visible}\n        aria-hidden={!visible}\n        onKeyDown={onKeyDown}\n        className=\"invisible absolute bottom-[calc(100%+8px)] left-0 flex w-56 origin-bottom-left flex-col gap-0.5 rounded-2xl border border-[var(--pc-border)] bg-[var(--pc-paper)] p-1.5 opacity-0 shadow-xl @min-[561px]/composer:top-[calc(100%+8px)] @min-[561px]/composer:bottom-auto @min-[561px]/composer:origin-top-left\"\n      >\n        <span\n          ref={highlightRef}\n          data-model-highlight\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute top-0 right-1.5 left-1.5 rounded-xl bg-[var(--pc-pill)] opacity-0\"\n        />\n        {models.map((model) => (\n          <button\n            key={model.id}\n            type=\"button\"\n            role=\"menuitemradio\"\n            data-model-option={model.id}\n            aria-checked={value.id === model.id}\n            tabIndex={visible && value.id === model.id ? 0 : -1}\n            onPointerEnter={(event) => {\n              if (event.pointerType === \"mouse\") setHovered(model.id);\n            }}\n            onFocus={() => setHovered(model.id)}\n            onClick={() => {\n              onChange(model);\n              close();\n              triggerRef.current?.focus({ preventScroll: true });\n            }}\n            className=\"relative z-10 flex min-h-12 cursor-pointer items-center justify-between gap-3 rounded-xl px-3 py-2 text-left outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[var(--pc-accent)]/40\"\n          >\n            <span className=\"flex min-w-0 flex-col gap-0.5\">\n              <span className=\"truncate text-xs font-medium\">{model.name}</span>\n              {model.description && (\n                <span className=\"truncate text-[11px] text-[var(--pc-muted)]\">\n                  {model.description}\n                </span>\n              )}\n            </span>\n            {value.id === model.id && (\n              <Check className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n            )}\n          </button>\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/prompt-composer-model.tsx"
    },
    {
      "path": "src/registry/ui/prompt-composer-recorder.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useEffectEvent, useRef, useState } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { LoaderCircle, Square, X } from \"lucide-react\";\nimport { useReducedMotion } from \"motion/react\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type PromptTranscriber = (\n  audio: Blob,\n  signal: AbortSignal,\n) => Promise<string>;\n\nconst BAR_COUNT = 40;\nconst DEMO_TRANSCRIPT =\n  \"Remind me to send the design review notes to Sam at 5 pm\";\n\nexport function PromptComposerRecorder({\n  demo,\n  onTranscribe,\n  onComplete,\n  onCancel,\n  onError,\n}: {\n  demo: boolean;\n  onTranscribe?: PromptTranscriber;\n  onComplete: (text: string) => void;\n  onCancel: () => void;\n  onError: (message: string) => void;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const levelsRef = useRef<Uint8Array<ArrayBuffer> | null>(null);\n  const analyserRef = useRef<AnalyserNode | null>(null);\n  const stopRef = useRef<(() => void) | null>(null);\n  const [phase, setPhase] = useState<\n    \"requesting\" | \"recording\" | \"transcribing\"\n  >(demo ? \"recording\" : \"requesting\");\n  const [seconds, setSeconds] = useState(0);\n  const reduced = useReducedMotion();\n  const complete = useEffectEvent(onComplete);\n  const fail = useEffectEvent(onError);\n\n  useEffect(() => {\n    const controller = new AbortController();\n    let stream: MediaStream | null = null;\n    let recorder: MediaRecorder | null = null;\n    let audioContext: AudioContext | null = null;\n    let demoDelay: gsap.core.Tween | null = null;\n    let started = 0;\n    let lastSecond = 0;\n    const clock = () => {\n      const elapsed = Math.floor((performance.now() - started) / 1000);\n      if (elapsed !== lastSecond) {\n        lastSecond = elapsed;\n        setSeconds(elapsed);\n      }\n    };\n    const beginClock = () => {\n      started = performance.now();\n      gsap.ticker.add(clock);\n    };\n    const stopTracks = () => {\n      stream?.getTracks().forEach((track) => track.stop());\n      gsap.ticker.remove(clock);\n    };\n    const transcribe = async (blob: Blob) => {\n      if (controller.signal.aborted) return;\n      stopTracks();\n      try {\n        if (!onTranscribe) throw new Error(\"Missing transcription provider\");\n        const transcript = await onTranscribe(blob, controller.signal);\n        if (controller.signal.aborted) return;\n        if (!transcript.trim()) {\n          fail(\"No speech was detected. Please try again.\");\n          return;\n        }\n        complete(transcript.trim());\n      } catch {\n        if (!controller.signal.aborted)\n          fail(\"Transcription failed. Your draft is saved. Please try again.\");\n      }\n    };\n    if (demo) {\n      beginClock();\n      stopRef.current = () => {\n        gsap.ticker.remove(clock);\n        setPhase(\"transcribing\");\n        demoDelay = gsap.delayedCall(0.65, () => complete(DEMO_TRANSCRIPT));\n      };\n    } else {\n      const start = async () => {\n        try {\n          if (\n            !navigator.mediaDevices?.getUserMedia ||\n            typeof MediaRecorder === \"undefined\"\n          ) {\n            fail(\n              \"Voice recording is unavailable in this browser. Please type your message.\",\n            );\n            return;\n          }\n          const media = await navigator.mediaDevices.getUserMedia({\n            audio: true,\n          });\n          if (controller.signal.aborted) {\n            media.getTracks().forEach((track) => track.stop());\n            return;\n          }\n          stream = media;\n          audioContext = new AudioContext();\n          const analyser = audioContext.createAnalyser();\n          analyser.fftSize = 256;\n          audioContext.createMediaStreamSource(media).connect(analyser);\n          analyserRef.current = analyser;\n          levelsRef.current = new Uint8Array(analyser.frequencyBinCount);\n          recorder = new MediaRecorder(media);\n          const chunks: Blob[] = [];\n          recorder.ondataavailable = (event) => {\n            if (event.data.size > 0) chunks.push(event.data);\n          };\n          recorder.onstop = () => {\n            void transcribe(\n              new Blob(chunks, { type: recorder?.mimeType || \"audio/webm\" }),\n            );\n          };\n          recorder.onerror = () => {\n            stopTracks();\n            if (!controller.signal.aborted)\n              fail(\"Recording failed. Please try again.\");\n          };\n          recorder.start();\n          beginClock();\n          setPhase(\"recording\");\n          stopRef.current = () => {\n            if (recorder?.state !== \"recording\") return;\n            setPhase(\"transcribing\");\n            recorder.stop();\n          };\n        } catch (error) {\n          stopTracks();\n          if (controller.signal.aborted) return;\n          fail(\n            error instanceof DOMException && error.name === \"NotAllowedError\"\n              ? \"Microphone access was denied. You can still type your message.\"\n              : \"Could not start recording. Please check your microphone and try again.\",\n          );\n        }\n      };\n      void start();\n    }\n    return () => {\n      controller.abort();\n      demoDelay?.kill();\n      stopRef.current = null;\n      if (recorder && recorder.state !== \"inactive\") recorder.stop();\n      stopTracks();\n      analyserRef.current = null;\n      levelsRef.current = null;\n      if (audioContext && audioContext.state !== \"closed\")\n        void audioContext.close();\n    };\n  }, [demo, onTranscribe]);\n\n  useGSAP(\n    () => {\n      const bars = Array.from(\n        rootRef.current?.querySelectorAll<HTMLElement>(\"[data-voice-bar]\") ??\n          [],\n      );\n      if (reduced || phase !== \"recording\") {\n        gsap.set(bars, {\n          scaleY: (index) => 0.12 + Math.abs(Math.sin(index * 0.8)) * 0.3,\n        });\n        return;\n      }\n      const levels = Array.from({ length: BAR_COUNT }, () => 0.08);\n      let previous = 0;\n      const paint = () => {\n        const time = performance.now();\n        if (time - previous < 75) return;\n        previous = time;\n        let level = 0.08;\n        if (demo) {\n          level =\n            0.12 + Math.abs(Math.sin(time / 190) * Math.sin(time / 470)) * 0.85;\n        } else if (analyserRef.current && levelsRef.current) {\n          const data = levelsRef.current;\n          analyserRef.current.getByteFrequencyData(data);\n          level = Math.max(\n            0.06,\n            data.reduce((sum, sample) => sum + sample, 0) / data.length / 150,\n          );\n        }\n        levels.shift();\n        levels.push(Math.min(1, level));\n        bars.forEach((bar, index) => {\n          bar.style.transform = `scaleY(${levels[index]})`;\n        });\n      };\n      gsap.ticker.add(paint);\n      gsap.to(\"[data-recording-dot]\", {\n        opacity: 0.35,\n        duration: 0.6,\n        repeat: -1,\n        yoyo: true,\n        ease: \"sine.inOut\",\n      });\n      return () => gsap.ticker.remove(paint);\n    },\n    {\n      scope: rootRef,\n      dependencies: [phase, reduced, demo],\n      revertOnUpdate: true,\n    },\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      className=\"flex h-full min-w-0 flex-1 items-center gap-3 pl-3 sm:pl-4\"\n      aria-busy={phase !== \"recording\"}\n    >\n      <span className=\"flex shrink-0 items-center gap-2 text-xs tabular-nums text-[var(--pc-muted)]\">\n        <span\n          data-recording-dot\n          className=\"size-2 rounded-full bg-red-400\"\n          aria-hidden=\"true\"\n        />\n        {Math.floor(seconds / 60)}:{String(seconds % 60).padStart(2, \"0\")}\n      </span>\n      <span className=\"sr-only\" role=\"status\">\n        {phase === \"requesting\"\n          ? \"Waiting for microphone permission\"\n          : phase === \"transcribing\"\n            ? \"Transcribing audio\"\n            : demo\n              ? \"Recording preview. The microphone is not active.\"\n              : \"Recording audio\"}\n      </span>\n      <span\n        className=\"flex h-7 min-w-0 flex-1 items-center justify-end gap-[3px] overflow-hidden text-[var(--pc-muted)]\"\n        aria-hidden=\"true\"\n      >\n        {phase === \"recording\" ? (\n          Array.from({ length: BAR_COUNT }, (_, index) => (\n            <span\n              key={index}\n              data-voice-bar\n              className=\"h-full w-0.5 shrink-0 origin-center rounded-full bg-current\"\n            />\n          ))\n        ) : (\n          <span className=\"truncate text-xs\">\n            {phase === \"requesting\" ? \"Connecting…\" : \"Transcribing…\"}\n          </span>\n        )}\n      </span>\n      <button\n        autoFocus\n        type=\"button\"\n        onClick={onCancel}\n        aria-label=\"Cancel recording\"\n        className=\"grid size-10 shrink-0 cursor-pointer place-items-center rounded-full outline-none hover:bg-[var(--pc-pill)] focus-visible:ring-2 focus-visible:ring-[var(--pc-accent)]\"\n      >\n        <X className=\"size-4\" />\n      </button>\n      <button\n        type=\"button\"\n        disabled={phase !== \"recording\"}\n        onClick={() => stopRef.current?.()}\n        aria-label=\"Stop recording\"\n        className=\"grid size-9 shrink-0 cursor-pointer place-items-center rounded-full bg-red-400/15 text-red-500 outline-none focus-visible:ring-2 focus-visible:ring-red-400 disabled:cursor-wait disabled:opacity-50\"\n      >\n        {phase === \"recording\" ? (\n          <Square className=\"size-3 fill-current\" />\n        ) : (\n          <LoaderCircle className=\"size-4 motion-safe:animate-spin\" />\n        )}\n      </button>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/prompt-composer-recorder.tsx"
    }
  ]
}
