{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "PromptBar-TS-TW",
	"title": "PromptBar",
	"description": "Chat composer with @ sources, / commands, a model picker, dictation and attachments.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "PromptBar/PromptBar.tsx",
			"content": "'use client';\n\nimport React, {\n  isValidElement,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactNode\n} from 'react';\nimport { animate, useMotionValue, useMotionValueEvent, useReducedMotion } from 'motion/react';\nimport { HugeiconsIcon, type IconSvgElement } from '@hugeicons/react';\nimport {\n  ArrowDown01Icon,\n  Attachment01Icon,\n  Calendar03Icon,\n  Cancel01Icon,\n  ChartLineData01Icon,\n  File02Icon,\n  Globe02Icon,\n  HelpCircleIcon,\n  Mail01Icon,\n  Mic01Icon,\n  PlusSignIcon,\n  SparklesIcon,\n  Tick02Icon\n} from '@hugeicons/core-free-icons';\nexport interface PromptBarSource {\n  key: string;\n  name: string;\n  description?: string;\n  icon?: ReactNode | IconSvgElement;\n  attach?: boolean;\n}\n\nexport interface PromptBarCommand {\n  key: string;\n  name: string;\n  description?: string;\n}\n\nexport interface PromptBarModel {\n  key: string;\n  name: string;\n  tag?: string;\n}\n\nexport interface PromptBarSendDetail {\n  attachments: string[];\n  model?: PromptBarModel;\n  effort?: string;\n}\n\nexport interface PromptBarProps {\n  placeholder?: string;\n  sources?: PromptBarSource[];\n  commands?: PromptBarCommand[];\n  models?: PromptBarModel[];\n  defaultModel?: string;\n  efforts?: string[];\n  defaultEffort?: string;\n  onEffortChange?: (effort: string) => void;\n  busy?: boolean;\n  onSend?: (text: string, detail: PromptBarSendDetail) => void;\n  onStop?: () => void;\n  onAttach?: () => string | string[] | void | Promise<string | string[] | void>;\n  onDictate?: () => string | void | Promise<string | void>;\n  background?: string;\n  color?: string;\n  menuBackground?: string;\n  sparkColor?: string;\n  sparkBoost?: number;\n  width?: number;\n  radius?: number;\n  maxRows?: number;\n  morphDuration?: number;\n  squash?: number;\n  tilt?: number;\n  pressScale?: number;\n  className?: string;\n}\n\ntype Row = {\n  key: string;\n  name: string;\n  description?: string;\n  tag?: string;\n  icon?: ReactNode | IconSvgElement;\n  attach?: boolean;\n};\ntype Token = { kind: 'at' | 'slash'; query: string; start: number };\ntype Latest = Pick<PromptBarProps, 'onSend' | 'onStop' | 'onAttach' | 'onDictate' | 'onEffortChange'>;\ntype Spark = {\n  x: number;\n  y: number;\n  r: number;\n  vy: number;\n  sway: number;\n  phase: number;\n  life: number;\n  span: number;\n};\n\ninterface SendGlyphProps {\n  busy: boolean;\n  morphDuration: number;\n  squash: number;\n  tilt: number;\n}\n\nconst ARROW_UP = [12, 4.5, 18.5, 11, 14.25, 11, 14.25, 19.5, 9.75, 19.5, 9.75, 11, 5.5, 11];\nconst SQUARE = [12, 6, 18, 6, 18, 12, 18, 18, 6, 18, 6, 12, 6, 6];\nconst EASE_IN_OUT: [number, number, number, number] = [0.77, 0, 0.175, 1];\nconst LINE = 22;\nconst EDGE = 11;\n\nconst DEFAULT_SOURCES: PromptBarSource[] = [\n  {\n    key: 'files',\n    name: 'Photos & files',\n    description: 'Upload from this device',\n    icon: Attachment01Icon,\n    attach: true\n  },\n  { key: 'web', name: 'Web search', description: 'Live results', icon: Globe02Icon },\n  { key: 'sales', name: 'Sales data', description: 'Revenue and churn', icon: ChartLineData01Icon },\n  { key: 'docs', name: 'Documents', description: 'Specs, notes, briefs', icon: File02Icon },\n  { key: 'mail', name: 'Mail', description: 'Read and draft mail', icon: Mail01Icon },\n  { key: 'calendar', name: 'Calendar', description: 'Events and availability', icon: Calendar03Icon }\n];\nconst DEFAULT_COMMANDS: PromptBarCommand[] = [\n  { key: 'summarize', name: '/summarize', description: 'Digest the thread so far' },\n  { key: 'compare', name: '/compare', description: 'Two options side by side' },\n  { key: 'draft', name: '/draft', description: 'Write a first version' },\n  { key: 'explain', name: '/explain', description: 'A plain-language walkthrough' },\n  { key: 'tasks', name: '/tasks', description: 'Turn this into a to-do list' }\n];\nconst DEFAULT_MODELS: PromptBarModel[] = [\n  { key: 'nova-3', name: 'Nova 3', tag: 'Flagship' },\n  { key: 'nova-mini', name: 'Nova Mini', tag: 'Fast' },\n  { key: 'nova-2', name: 'Nova 2', tag: 'Legacy' }\n];\nconst DEFAULT_EFFORTS = ['Low', 'Medium', 'High', 'Extra', 'Max'];\n\nconst mix = (a: number, b: number, t: number) => a + (b - a) * t;\nconst pathAt = (a: number[], b: number[], t: number) => {\n  let d = '';\n  for (let i = 0; i < a.length; i += 2) {\n    d += `${i ? 'L' : 'M'}${mix(a[i], b[i], t).toFixed(2)} ${mix(a[i + 1], b[i + 1], t).toFixed(2)}`;\n  }\n  return `${d}Z`;\n};\n\nconst parseToken = (draft: string): Token | null => {\n  const m = /(^|\\s)([@/])([\\w-]*)$/.exec(draft);\n  if (!m) return null;\n  return { kind: m[2] === '@' ? 'at' : 'slash', query: m[3].toLowerCase(), start: m.index + m[1].length };\n};\n\nconst renderIcon = (icon: ReactNode | IconSvgElement, size: number) =>\n  isValidElement(icon) ? icon : <HugeiconsIcon icon={icon as IconSvgElement} size={size} strokeWidth={1.8} />;\n\nfunction SendGlyph({ busy, morphDuration, squash, tilt }: SendGlyphProps) {\n  const reduce = useReducedMotion();\n  const svgRef = useRef<SVGSVGElement>(null);\n  const pathRef = useRef<SVGPathElement>(null);\n  const dir = useRef(busy ? 1 : -1);\n  const t = useMotionValue(busy ? 1 : 0);\n\n  useEffect(() => {\n    const target = busy ? 1 : 0;\n    dir.current = busy ? 1 : -1;\n    if (t.get() === target) return undefined;\n    const controls = animate(\n      t,\n      target,\n      reduce ? { duration: 0 } : { duration: morphDuration / 1000, ease: EASE_IN_OUT }\n    );\n    return () => controls.stop();\n  }, [busy, morphDuration, reduce, t]);\n\n  useMotionValueEvent(t, 'change', v => {\n    pathRef.current?.setAttribute('d', pathAt(ARROW_UP, SQUARE, v));\n    const goo = reduce ? 0 : Math.sin(v * Math.PI);\n    const sx = 1 - squash * goo;\n    if (svgRef.current) {\n      svgRef.current.style.transform = goo ? `rotate(${dir.current * tilt * goo}deg) scale(${sx}, ${1 / sx})` : '';\n    }\n  });\n\n  return (\n    <svg\n      ref={svgRef}\n      className=\"block h-4 w-4 origin-center\"\n      viewBox=\"0 0 24 24\"\n      aria-hidden=\"true\"\n      fill=\"currentColor\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinejoin=\"round\"\n    >\n      <path ref={pathRef} d={pathAt(ARROW_UP, SQUARE, t.get())} />\n    </svg>\n  );\n}\n\nconst STYLE = `\n@keyframes prompt-bar-pop { from { opacity: 0; transform: translateY(4px) scale(0.98); } }\n@keyframes prompt-bar-eq { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }\n`;\n\nconst PromptBar: React.FC<PromptBarProps> = ({\n  placeholder = 'Ask anything',\n  sources = DEFAULT_SOURCES,\n  commands = DEFAULT_COMMANDS,\n  models = DEFAULT_MODELS,\n  defaultModel = '',\n  efforts = DEFAULT_EFFORTS,\n  defaultEffort = '',\n  onEffortChange,\n  busy = false,\n  onSend,\n  onStop,\n  onAttach,\n  onDictate,\n  background = '#27272a',\n  color = '#f5f5f5',\n  menuBackground = '#323236',\n  sparkColor = '#b39dff',\n  sparkBoost = 1,\n  width = 400,\n  radius = 16,\n  maxRows = 5,\n  morphDuration = 240,\n  squash = 0.12,\n  tilt = 8,\n  pressScale = 0.96,\n  className = ''\n}) => {\n  const reduce = useReducedMotion();\n  const rootRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const glowRef = useRef<HTMLSpanElement>(null);\n  const sparkRef = useRef<HTMLCanvasElement>(null);\n  const typing = useRef({ energy: 0, strokes: 0 });\n  const boost = useRef(sparkBoost);\n  boost.current = sparkBoost;\n  const rowRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const lastOpen = useRef<string | null>(null);\n  const dictation = useRef(0);\n  const latest = useRef<Latest>({});\n  latest.current = { onSend, onStop, onAttach, onDictate, onEffortChange };\n\n  const [draft, setDraft] = useState('');\n  const [attachments, setAttachments] = useState<string[]>([]);\n  const [modelKey, setModelKey] = useState(defaultModel);\n  const [plusOpen, setPlusOpen] = useState(false);\n  const [modelOpen, setModelOpen] = useState(false);\n  const [effortOpen, setEffortOpen] = useState(false);\n  const [effortIndex, setEffortIndex] = useState(() => {\n    const i = efforts.indexOf(defaultEffort);\n    return i >= 0 ? i : Math.max(0, Math.floor((efforts.length - 1) / 2));\n  });\n  const [dismissed, setDismissed] = useState(false);\n  const [active, setActive] = useState(0);\n  const [listening, setListening] = useState(false);\n  const [pressed, setPressed] = useState(false);\n\n  const model = models.find(m => m.key === modelKey) ?? models[0];\n  const token = dismissed ? null : parseToken(draft);\n  const open = plusOpen ? 'at' : (token?.kind ?? (modelOpen ? 'model' : effortOpen ? 'effort' : null));\n  const query = plusOpen ? '' : (token?.query ?? '');\n  const list = useMemo<Row[]>(() => {\n    if (open === 'at') return sources.filter(s => s.name.toLowerCase().includes(query));\n    if (open === 'slash') return commands.filter(c => c.name.replace(/^\\//, '').toLowerCase().startsWith(query));\n    if (open === 'model') return models;\n    return [];\n  }, [open, query, sources, commands, models]);\n  const cursor = Math.min(active, Math.max(0, list.length - 1));\n  const canSend = draft.trim().length > 0 || attachments.length > 0;\n  const armed = busy || canSend;\n  const level = efforts[effortIndex] ?? '';\n  const maxed = efforts.length > 1 && effortIndex === efforts.length - 1;\n\n  const focusInput = () => inputRef.current?.focus({ preventScroll: true });\n  const closeMenus = useCallback(() => {\n    setPlusOpen(false);\n    setModelOpen(false);\n    setEffortOpen(false);\n  }, []);\n\n  useLayoutEffect(() => {\n    const glow = glowRef.current;\n    if (!glow || !open) return;\n    const row = rowRefs.current[cursor];\n    if (!row) {\n      glow.style.opacity = '0';\n      return;\n    }\n    const fresh = lastOpen.current !== open;\n    lastOpen.current = open;\n    if (fresh) glow.style.transition = 'none';\n    glow.style.top = `${row.offsetTop}px`;\n    glow.style.height = `${row.offsetHeight}px`;\n    glow.style.opacity = '1';\n    if (fresh) {\n      void glow.offsetHeight;\n      glow.style.transition = '';\n    }\n  }, [open, cursor, list]);\n  useEffect(() => {\n    if (!open) lastOpen.current = null;\n  }, [open]);\n\n  useEffect(() => {\n    if (!plusOpen && !modelOpen && !effortOpen) return undefined;\n    const onDown = (e: globalThis.PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) closeMenus();\n    };\n    document.addEventListener('pointerdown', onDown);\n    return () => document.removeEventListener('pointerdown', onDown);\n  }, [plusOpen, modelOpen, effortOpen, closeMenus]);\n\n  useLayoutEffect(() => {\n    const el = inputRef.current;\n    if (!el) return;\n    el.style.height = '0px';\n    const max = LINE * maxRows;\n    el.style.height = `${Math.min(el.scrollHeight, max)}px`;\n    el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden';\n  }, [draft, maxRows]);\n\n  useEffect(\n    () => () => {\n      dictation.current += 1;\n    },\n    []\n  );\n\n  useEffect(() => {\n    const canvas = sparkRef.current;\n    if (!maxed || reduce || !canvas) return undefined;\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return undefined;\n    typing.current.strokes = 0;\n    let raf = 0;\n    let last = performance.now();\n    let w = 0;\n    let h = 0;\n    let due = 0;\n    let speed = 1;\n    let pulse = 0;\n    const parts: Spark[] = [];\n    let resizePending = false;\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect();\n      const dpr = Math.min(2, window.devicePixelRatio || 1);\n      w = rect.width;\n      h = rect.height;\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    };\n    const spawn = (burst: boolean) => {\n      parts.push({\n        x: Math.random() * w,\n        y: burst ? h * (0.2 + Math.random() * 0.8) : h + 3,\n        r: 0.9 + Math.random() * 1.1,\n        vy: -(7 + Math.random() * 9),\n        sway: (Math.random() - 0.5) * 10,\n        phase: Math.random() * Math.PI * 2,\n        life: burst ? Math.random() * 1.2 : 0,\n        span: 2.4 + Math.random() * 2.4\n      });\n    };\n    const tick = (now: number) => {\n      if (resizePending) {\n        resizePending = false;\n        resize();\n      }\n      const dt = Math.min(0.05, (now - last) / 1000);\n      last = now;\n      const typed = typing.current;\n      const gain = boost.current;\n      typed.energy *= Math.exp(-dt / 0.8);\n      pulse *= Math.exp(-dt / 0.16);\n      if (typed.strokes > 0) {\n        typed.strokes = 0;\n        if (gain > 0) pulse = 1;\n      }\n      const energy = typed.energy * gain;\n      speed += (1 + energy * 6 - speed) * (1 - Math.exp(-dt / 0.15));\n      due += dt;\n      while (due > 0.14) {\n        due -= 0.14;\n        if (parts.length < 30) spawn(false);\n      }\n      ctx.clearRect(0, 0, w, h);\n      ctx.fillStyle = sparkColor;\n      ctx.shadowColor = sparkColor;\n      ctx.shadowBlur = 6 + energy * 10 + pulse * 6;\n      for (let i = parts.length - 1; i >= 0; i -= 1) {\n        const p = parts[i];\n        p.life += dt;\n        if (p.life > p.span) {\n          parts.splice(i, 1);\n          continue;\n        }\n        const k = p.life / p.span;\n        const twinkle = 0.7 + 0.3 * Math.sin((now / 160) * (1 + energy) + p.phase);\n        p.y += p.vy * dt * speed;\n        if (p.y < -4) {\n          p.y = h + 3;\n          p.x = Math.random() * w;\n        }\n        const edge = Math.min(1, Math.max(0, p.y / 14), Math.max(0, (h - p.y) / 14));\n        ctx.globalAlpha = Math.min(1, Math.sin(k * Math.PI) * (0.9 + energy * 0.25) * twinkle) * edge;\n        ctx.beginPath();\n        ctx.arc(\n          p.x + Math.sin((now / 900) * (1 + energy * 0.8) + p.phase) * p.sway,\n          p.y,\n          p.r * twinkle * (1 + energy * 0.35),\n          0,\n          Math.PI * 2\n        );\n        ctx.fill();\n      }\n      raf = requestAnimationFrame(tick);\n    };\n    resize();\n    for (let i = 0; i < 26; i += 1) spawn(true);\n    const ro = new ResizeObserver(() => {\n      resizePending = true;\n    });\n    ro.observe(canvas);\n    raf = requestAnimationFrame(tick);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      ctx.clearRect(0, 0, w, h);\n    };\n  }, [maxed, reduce, sparkColor]);\n\n  const setEffort = (i: number) => {\n    const next = Math.max(0, Math.min(efforts.length - 1, i));\n    if (next === effortIndex) return;\n    setEffortIndex(next);\n    latest.current.onEffortChange?.(efforts[next]);\n  };\n  const effortFromPointer = (e: PointerEvent<HTMLDivElement>) => {\n    const rect = e.currentTarget.getBoundingClientRect();\n    const k = (e.clientX - rect.left - EDGE) / Math.max(1, rect.width - 2 * EDGE);\n    setEffort(Math.round(k * (efforts.length - 1)));\n  };\n  const onEffortKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    const step =\n      e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -1 : 0;\n    if (step) {\n      e.preventDefault();\n      setEffort(effortIndex + step);\n    } else if (e.key === 'Home') {\n      e.preventDefault();\n      setEffort(0);\n    } else if (e.key === 'End') {\n      e.preventDefault();\n      setEffort(efforts.length - 1);\n    } else if (e.key === 'Escape') {\n      setEffortOpen(false);\n      focusInput();\n    }\n  };\n  const stepAt = (i: number) => `calc(${EDGE}px + (100% - ${EDGE * 2}px) * ${i / Math.max(1, efforts.length - 1)})`;\n  const fillAt = (i: number) => (i === efforts.length - 1 ? '100%' : `calc(${stepAt(i)} + 7px)`);\n\n  const pick = (row: Row) => {\n    if (open === 'model') {\n      setModelKey(row.key);\n      setModelOpen(false);\n      focusInput();\n      return;\n    }\n    const head = token ? draft.slice(0, token.start) : draft;\n    if (row.attach) {\n      setDraft(head);\n      Promise.resolve(latest.current.onAttach?.()).then(files => {\n        if (!files) return;\n        setAttachments(a => [...a, ...(Array.isArray(files) ? files : [files])]);\n      });\n    } else if (open === 'at') {\n      setDraft(`${head}@${row.name} `);\n    } else {\n      setDraft(`${head}${row.name} `);\n    }\n    setPlusOpen(false);\n    setDismissed(false);\n    focusInput();\n  };\n\n  const send = () => {\n    if (!canSend || busy) return;\n    latest.current.onSend?.(draft.trim(), { attachments, model, effort: level });\n    setDraft('');\n    setAttachments([]);\n    setDismissed(false);\n    closeMenus();\n    focusInput();\n  };\n\n  const toggleListen = () => {\n    if (listening) {\n      dictation.current += 1;\n      setListening(false);\n      return;\n    }\n    const seq = ++dictation.current;\n    setListening(true);\n    Promise.resolve(latest.current.onDictate?.()).then(\n      text => {\n        if (seq !== dictation.current) return;\n        setListening(false);\n        if (text) setDraft(d => (d.trim() ? `${d.trimEnd()} ${text}` : text));\n        focusInput();\n      },\n      () => {\n        if (seq === dictation.current) setListening(false);\n      }\n    );\n  };\n\n  const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {\n    if (open && list.length) {\n      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n        e.preventDefault();\n        setActive((cursor + (e.key === 'ArrowDown' ? 1 : list.length - 1)) % list.length);\n        return;\n      }\n      if ((e.key === 'Enter' && !e.shiftKey) || e.key === 'Tab') {\n        e.preventDefault();\n        pick(list[cursor]);\n        return;\n      }\n    }\n    if (e.key === 'Escape') {\n      if (open) {\n        e.preventDefault();\n        setDismissed(true);\n        closeMenus();\n      }\n      return;\n    }\n    if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {\n      e.preventDefault();\n      send();\n    }\n  };\n\n  const down = (e: PointerEvent<HTMLButtonElement>) => {\n    if (e.button !== 0 || !armed) return;\n    setPressed(true);\n  };\n  const up = () => setPressed(false);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`group relative text-[14px] leading-[22px] [width:min(var(--pb-w),100%)] [color:var(--pb-ink)]${className ? ` ${className}` : ''}`}\n      data-busy={busy ? '' : undefined}\n      data-max={maxed ? '' : undefined}\n      style={\n        {\n          '--pb-bg': background,\n          '--pb-ink': color,\n          '--pb-menu': menuBackground,\n          '--pb-w': `${width}px`,\n          '--pb-radius': `${radius}px`,\n          '--pb-spark': sparkColor,\n          '--pb-press': pressScale\n        } as CSSProperties\n      }\n    >\n      <style>{STYLE}</style>\n      {open ? (\n        <div\n          className=\"absolute inset-x-0 bottom-[calc(100%+8px)] z-[2] origin-bottom rounded-xl p-1 shadow-[0_10px_30px_-10px_rgba(0,0,0,0.35),0_1px_2px_rgba(0,0,0,0.08)] [animation:prompt-bar-pop_180ms_cubic-bezier(0.23,1,0.32,1)_both] [background:var(--pb-menu)] data-[kind=model]:right-auto data-[kind=model]:w-[200px] data-[kind=model]:origin-bottom-left data-[kind=effort]:right-auto data-[kind=effort]:w-[248px] data-[kind=effort]:origin-bottom-left data-[kind=effort]:px-3.5 data-[kind=effort]:pt-3 data-[kind=effort]:pb-3.5 motion-reduce:[animation:none]\"\n          role={open === 'effort' ? 'dialog' : 'listbox'}\n          aria-label={\n            open === 'at' ? 'Sources' : open === 'slash' ? 'Commands' : open === 'model' ? 'Models' : 'Effort'\n          }\n          data-kind={open}\n        >\n          {open === 'effort' ? (\n            <>\n              <div className=\"flex items-center gap-2 text-[13px] leading-[18px]\">\n                <span className=\"[color:color-mix(in_srgb,var(--pb-ink)_55%,transparent)]\">Effort</span>\n                <span className=\"font-medium\">{level}</span>\n                <span\n                  className=\"ml-auto inline-flex [color:color-mix(in_srgb,var(--pb-ink)_55%,transparent)]\"\n                  title=\"Higher effort thinks longer before answering\"\n                >\n                  <HugeiconsIcon icon={HelpCircleIcon} size={14} strokeWidth={1.8} />\n                </span>\n              </div>\n              <div className=\"mt-3 flex justify-between text-[12px] leading-4 [color:color-mix(in_srgb,var(--pb-ink)_55%,transparent)]\">\n                <span>Faster</span>\n                <span>Smarter</span>\n              </div>\n              <div\n                className=\"relative mt-2 h-[22px] cursor-pointer touch-none rounded-[11px] outline-none select-none [background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)]\"\n                role=\"slider\"\n                tabIndex={0}\n                aria-label=\"Effort\"\n                aria-valuemin={0}\n                aria-valuemax={efforts.length - 1}\n                aria-valuenow={effortIndex}\n                aria-valuetext={level}\n                style={\n                  { '--pb-effort-x': stepAt(effortIndex), '--pb-effort-fill': fillAt(effortIndex) } as CSSProperties\n                }\n                onPointerDown={e => {\n                  if (e.button !== 0) return;\n                  try {\n                    e.currentTarget.setPointerCapture(e.pointerId);\n                  } catch {}\n                  e.currentTarget.focus({ preventScroll: true });\n                  effortFromPointer(e);\n                }}\n                onPointerMove={e => {\n                  if (e.buttons & 1) effortFromPointer(e);\n                }}\n                onKeyDown={onEffortKey}\n              >\n                <span className=\"absolute inset-y-0 left-0 rounded-[11px] [width:var(--pb-effort-fill)] [background:color-mix(in_srgb,var(--pb-ink)_18%,transparent)] [transition:width_220ms_cubic-bezier(0.23,1,0.32,1),background-color_300ms_ease] group-data-[max]:[background:color-mix(in_srgb,var(--pb-spark)_35%,transparent)] motion-reduce:[transition:background-color_300ms_ease]\" />\n                {efforts.map((label, i) => (\n                  <i\n                    key={label}\n                    className=\"absolute top-1/2 -mt-0.5 -ml-0.5 h-1 w-1 rounded-full [background:color-mix(in_srgb,var(--pb-ink)_30%,transparent)]\"\n                    style={{ left: stepAt(i) }}\n                  />\n                ))}\n                <span className=\"absolute -top-[3px] -ml-[7px] h-7 w-3.5 rounded-[7px] shadow-[0_2px_6px_rgba(0,0,0,0.25)] [left:var(--pb-effort-x)] [background:var(--pb-ink)] [transition:left_220ms_cubic-bezier(0.23,1,0.32,1),background-color_300ms_ease] group-data-[max]:[background:var(--pb-spark)] motion-reduce:[transition:background-color_300ms_ease]\" />\n              </div>\n            </>\n          ) : (\n            <>\n              <span\n                ref={glowRef}\n                className=\"pointer-events-none absolute inset-x-1 rounded-lg opacity-0 [background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] [transition:top_220ms_cubic-bezier(0.23,1,0.32,1),height_220ms_cubic-bezier(0.23,1,0.32,1),opacity_150ms_ease] motion-reduce:[transition:opacity_150ms_ease]\"\n                aria-hidden=\"true\"\n              />\n              {list.map((row, i) => (\n                <button\n                  key={row.key}\n                  ref={el => {\n                    rowRefs.current[i] = el;\n                  }}\n                  type=\"button\"\n                  role=\"option\"\n                  aria-selected={i === cursor}\n                  className=\"relative z-[1] flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2 text-left text-inherit outline-none [font:inherit] [-webkit-tap-highlight-color:transparent]\"\n                  onMouseDown={e => e.preventDefault()}\n                  onPointerEnter={() => setActive(i)}\n                  onClick={() => pick(row)}\n                >\n                  {open === 'at' ? (\n                    <span className=\"inline-flex w-5 flex-none justify-center [color:color-mix(in_srgb,var(--pb-ink)_70%,transparent)]\">\n                      {renderIcon(row.icon, 15)}\n                    </span>\n                  ) : null}\n                  <span className=\"flex-none text-[13px] font-medium\">{row.name}</span>\n                  {row.description ? (\n                    <span className=\"min-w-0 flex-auto truncate text-[12px] [color:color-mix(in_srgb,var(--pb-ink)_55%,transparent)]\">\n                      {row.description}\n                    </span>\n                  ) : null}\n                  {open === 'model' ? (\n                    <>\n                      <span className=\"ml-auto flex-none text-[11px] [color:color-mix(in_srgb,var(--pb-ink)_55%,transparent)]\">\n                        {row.tag}\n                      </span>\n                      <span\n                        className=\"inline-flex w-4 flex-none justify-center opacity-0 data-[on]:opacity-100\"\n                        data-on={row.key === model?.key ? '' : undefined}\n                      >\n                        <HugeiconsIcon icon={Tick02Icon} size={13} strokeWidth={2.5} />\n                      </span>\n                    </>\n                  ) : null}\n                </button>\n              ))}\n              {list.length === 0 ? (\n                <div className=\"flex h-9 items-center px-2 text-[12px] [color:color-mix(in_srgb,var(--pb-ink)_55%,transparent)]\">\n                  No matches for “{query}”\n                </div>\n              ) : null}\n            </>\n          )}\n        </div>\n      ) : null}\n\n      <div\n        className=\"relative isolate flex cursor-text flex-col gap-2 p-3 [background:var(--pb-bg)] [border-radius:var(--pb-radius)] before:pointer-events-none before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] before:opacity-0 before:content-[''] before:[background:radial-gradient(140%_120%_at_0%_100%,color-mix(in_srgb,var(--pb-spark)_26%,transparent),transparent_62%)] before:[transition:opacity_500ms_ease] data-[max]:before:opacity-100\"\n        role=\"presentation\"\n        data-max={maxed ? '' : undefined}\n        onPointerDown={e => {\n          if (e.target === e.currentTarget || e.target === inputRef.current) closeMenus();\n        }}\n        onClick={focusInput}\n      >\n        <canvas\n          ref={sparkRef}\n          className=\"pointer-events-none absolute inset-0 -z-10 h-full w-full rounded-[inherit]\"\n          aria-hidden=\"true\"\n        />\n        {attachments.length > 0 ? (\n          <div className=\"flex flex-wrap gap-1.5\">\n            {attachments.map((file, i) => (\n              <span\n                key={`${file}-${i}`}\n                className=\"inline-flex h-[26px] items-center gap-1.5 rounded-lg pr-1 pl-2 text-[12px] [animation:prompt-bar-pop_200ms_cubic-bezier(0.23,1,0.32,1)_both] [background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] motion-reduce:[animation:none]\"\n              >\n                <HugeiconsIcon icon={File02Icon} size={12} strokeWidth={2} />\n                <span className=\"max-w-[144px] truncate\">{file}</span>\n                <button\n                  type=\"button\"\n                  className=\"inline-grid h-[18px] w-[18px] cursor-pointer place-items-center rounded-[5px] border-0 bg-transparent p-0 text-inherit opacity-60 outline-none [transition:opacity_120ms_ease,background-color_120ms_ease] hover:opacity-100 hover:[background:color-mix(in_srgb,var(--pb-ink)_10%,transparent)]\"\n                  aria-label={`Remove ${file}`}\n                  onClick={() => setAttachments(a => a.filter((_, j) => j !== i))}\n                >\n                  <HugeiconsIcon icon={Cancel01Icon} size={10} strokeWidth={2.5} />\n                </button>\n              </span>\n            ))}\n          </div>\n        ) : null}\n\n        <textarea\n          ref={inputRef}\n          className=\"block w-full resize-none border-0 bg-transparent p-0 text-[14px] leading-[22px] text-inherit outline-none [font:inherit] [overflow-wrap:anywhere] placeholder:[color:color-mix(in_srgb,var(--pb-ink)_45%,transparent)] [@media(pointer:coarse)]:text-[16px]\"\n          rows={1}\n          value={draft}\n          placeholder={listening ? 'Listening…' : placeholder}\n          aria-label=\"Prompt\"\n          onChange={e => {\n            setDraft(e.target.value);\n            typing.current.energy = Math.min(1.6, typing.current.energy + 0.22);\n            typing.current.strokes = Math.min(4, typing.current.strokes + 1);\n            setDismissed(false);\n            closeMenus();\n            setActive(0);\n          }}\n          onFocus={closeMenus}\n          onKeyDown={onKeyDown}\n        />\n\n        <div className=\"flex items-center gap-1\">\n          <button\n            type=\"button\"\n            className=\"inline-grid h-7 w-7 flex-none cursor-pointer touch-manipulation place-items-center rounded-lg border-0 bg-transparent p-0 outline-none select-none [color:color-mix(in_srgb,var(--pb-ink)_60%,transparent)] [font:inherit] [-webkit-tap-highlight-color:transparent] [transition:background-color_150ms_ease,color_150ms_ease,transform_160ms_cubic-bezier(0.23,1,0.32,1)] active:[transform:scale(0.94)] data-[on]:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] data-[on]:[color:var(--pb-ink)] motion-reduce:active:[transform:none] [@media(hover:hover)_and_(pointer:fine)]:hover:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] [@media(hover:hover)_and_(pointer:fine)]:hover:[color:var(--pb-ink)]\"\n            aria-label=\"Add files and sources\"\n            aria-expanded={plusOpen}\n            data-on={plusOpen ? '' : undefined}\n            onMouseDown={e => e.preventDefault()}\n            onClick={() => {\n              setModelOpen(false);\n              setEffortOpen(false);\n              setActive(0);\n              setPlusOpen(v => !v);\n              focusInput();\n            }}\n          >\n            <HugeiconsIcon icon={PlusSignIcon} size={16} strokeWidth={2} />\n          </button>\n          {models.length > 0 ? (\n            <button\n              type=\"button\"\n              className=\"inline-flex h-7 flex-none cursor-pointer touch-manipulation items-center gap-1 rounded-lg border-0 bg-transparent px-2 text-[12px] font-medium outline-none select-none [color:color-mix(in_srgb,var(--pb-ink)_70%,transparent)] [font:inherit] [-webkit-tap-highlight-color:transparent] [transition:background-color_150ms_ease,color_150ms_ease] data-[on]:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] data-[on]:[color:var(--pb-ink)] [@media(hover:hover)_and_(pointer:fine)]:hover:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] [@media(hover:hover)_and_(pointer:fine)]:hover:[color:var(--pb-ink)] data-[max]:[color:var(--pb-spark)]! \"\n              aria-label=\"Choose model\"\n              aria-expanded={modelOpen}\n              data-on={modelOpen ? '' : undefined}\n              onMouseDown={e => e.preventDefault()}\n              onClick={() => {\n                setPlusOpen(false);\n                setEffortOpen(false);\n                setActive(Math.max(0, models.indexOf(model)));\n                setModelOpen(v => !v);\n                focusInput();\n              }}\n            >\n              <span>{model.name}</span>\n              <HugeiconsIcon icon={ArrowDown01Icon} size={12} strokeWidth={2.4} />\n            </button>\n          ) : null}\n          {efforts.length > 0 ? (\n            <button\n              type=\"button\"\n              className=\"inline-flex h-7 flex-none cursor-pointer touch-manipulation items-center gap-1 rounded-lg border-0 bg-transparent px-2 text-[12px] font-medium outline-none select-none [color:color-mix(in_srgb,var(--pb-ink)_70%,transparent)] [font:inherit] [-webkit-tap-highlight-color:transparent] [transition:background-color_150ms_ease,color_150ms_ease] data-[on]:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] data-[on]:[color:var(--pb-ink)] [@media(hover:hover)_and_(pointer:fine)]:hover:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] [@media(hover:hover)_and_(pointer:fine)]:hover:[color:var(--pb-ink)] data-[max]:[color:var(--pb-spark)]! \"\n              aria-label=\"Choose effort\"\n              aria-expanded={effortOpen}\n              data-on={effortOpen ? '' : undefined}\n              data-max={maxed ? '' : undefined}\n              onMouseDown={e => e.preventDefault()}\n              onClick={() => {\n                setPlusOpen(false);\n                setModelOpen(false);\n                setEffortOpen(v => !v);\n                focusInput();\n              }}\n            >\n              <HugeiconsIcon icon={SparklesIcon} size={13} strokeWidth={2} />\n              <span>{level}</span>\n            </button>\n          ) : null}\n          <span className=\"flex-auto\" />\n          {onDictate ? (\n            <button\n              type=\"button\"\n              className=\"inline-grid h-7 w-7 flex-none cursor-pointer touch-manipulation place-items-center rounded-lg border-0 bg-transparent p-0 outline-none select-none [color:color-mix(in_srgb,var(--pb-ink)_60%,transparent)] [font:inherit] [-webkit-tap-highlight-color:transparent] [transition:background-color_150ms_ease,color_150ms_ease,transform_160ms_cubic-bezier(0.23,1,0.32,1)] active:[transform:scale(0.94)] data-[on]:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] data-[on]:[color:var(--pb-ink)] motion-reduce:active:[transform:none] [@media(hover:hover)_and_(pointer:fine)]:hover:[background:color-mix(in_srgb,var(--pb-ink)_8%,transparent)] [@media(hover:hover)_and_(pointer:fine)]:hover:[color:var(--pb-ink)]\"\n              aria-label={listening ? 'Stop dictation' : 'Dictate'}\n              aria-pressed={listening}\n              data-on={listening ? '' : undefined}\n              onMouseDown={e => e.preventDefault()}\n              onClick={toggleListen}\n            >\n              {listening ? (\n                <span\n                  className=\"flex h-3.5 items-center gap-[2.5px] [&>i]:block [&>i]:h-full [&>i]:w-[2.5px] [&>i]:origin-center [&>i]:rounded-full [&>i]:bg-current [&>i]:[animation:prompt-bar-eq_900ms_ease-in-out_infinite] [&>i:nth-child(2)]:[animation-delay:150ms] [&>i:nth-child(3)]:[animation-delay:300ms]\"\n                  aria-hidden=\"true\"\n                >\n                  <i />\n                  <i />\n                  <i />\n                </span>\n              ) : (\n                <HugeiconsIcon icon={Mic01Icon} size={15} strokeWidth={2} />\n              )}\n            </button>\n          ) : null}\n          <button\n            type=\"button\"\n            className=\"relative inline-grid h-7 w-7 flex-none cursor-pointer touch-manipulation place-items-center rounded-lg border-0 p-0 outline-none select-none [background:color-mix(in_srgb,var(--pb-ink)_12%,var(--pb-bg))] [color:color-mix(in_srgb,var(--pb-ink)_55%,var(--pb-bg))] [font:inherit] [-webkit-tap-highlight-color:transparent] [transition:background-color_200ms_ease,color_200ms_ease,transform_160ms_cubic-bezier(0.23,1,0.32,1)] disabled:cursor-default data-[armed]:[background:var(--pb-ink)] data-[armed]:[color:var(--pb-bg)] data-[pressed]:[transform:scale(var(--pb-press))] motion-reduce:data-[pressed]:[transform:none]\"\n            disabled={!armed}\n            aria-label={busy ? 'Stop' : 'Send'}\n            data-armed={armed ? '' : undefined}\n            data-pressed={pressed ? '' : undefined}\n            onMouseDown={e => e.preventDefault()}\n            onPointerDown={down}\n            onPointerUp={up}\n            onPointerCancel={up}\n            onPointerLeave={up}\n            onClick={() => {\n              if (busy) latest.current.onStop?.();\n              else send();\n            }}\n          >\n            <SendGlyph busy={busy} morphDuration={morphDuration} squash={squash} tilt={tilt} />\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default PromptBar;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12",
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}