{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "morph-panel",
  "type": "registry:ui",
  "title": "Morph panel",
  "description": "A React button that morphs into the panel it opens on a spring, blurring the label out and the form in. Ships with a feedback form and closes on Escape.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/morph-panel.tsx",
      "type": "registry:ui",
      "target": "components/ui/morph-panel.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { CheckIcon, MessageSquareIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * MorphPanel: the button becomes the thing it opens. One surface stretches\n * from a pill into a panel on a spring, its corners easing from 10 to 16px,\n * while the label blurs out and the form blurs in. No popover, no jump.\n * Default content is a feedback form with a sent state.\n */\n\ntype Anchor = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"\n\ntype MorphPanelProps = {\n  label?: React.ReactNode\n  icon?: React.ReactNode\n  /** Panel width in px. */\n  width?: number\n  /** Which corner of the trigger the panel grows from. */\n  anchor?: Anchor\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  /** Custom panel. Receives close(). */\n  children?: (api: { close: () => void }) => React.ReactNode\n  /** Called by the default feedback form. */\n  onSubmit?: (text: string) => void | Promise<void>\n  className?: string\n}\n\nconst SPRING = { type: \"spring\", bounce: 0.16, duration: 0.5 } as const\nconst POS: Record<Anchor, string> = {\n  \"top-left\": \"top-0 left-0 origin-top-left\",\n  \"top-right\": \"top-0 right-0 origin-top-right\",\n  \"bottom-left\": \"bottom-0 left-0 origin-bottom-left\",\n  \"bottom-right\": \"bottom-0 right-0 origin-bottom-right\",\n}\n\nfunction MorphPanel({\n  label = \"Feedback\",\n  icon = <MessageSquareIcon />,\n  width = 340,\n  anchor = \"top-left\",\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  children,\n  onSubmit,\n  className,\n}: MorphPanelProps) {\n  const reduce = useReducedMotion()\n  const [inner, setInner] = React.useState(defaultOpen)\n  const open = openProp ?? inner\n  const setOpen = (v: boolean) => {\n    if (openProp === undefined) setInner(v)\n    onOpenChange?.(v)\n  }\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n  const t = reduce ? { duration: 0 } : SPRING\n\n  const close = React.useCallback(() => {\n    setOpen(false)\n    requestAnimationFrame(() => triggerRef.current?.focus())\n  }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n  // Outside click and Escape close the panel.\n  React.useEffect(() => {\n    if (!open) return\n    const onDown = (e: PointerEvent) => {\n      if (rootRef.current && !rootRef.current.contains(e.target as Node)) close()\n    }\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close()\n    document.addEventListener(\"pointerdown\", onDown)\n    document.addEventListener(\"keydown\", onKey)\n    return () => {\n      document.removeEventListener(\"pointerdown\", onDown)\n      document.removeEventListener(\"keydown\", onKey)\n    }\n  }, [open, close])\n\n  return (\n    <div ref={rootRef} data-slot=\"morph-panel\" data-state={open ? \"open\" : \"closed\"} className={cn(\"relative inline-block h-9\", className)}>\n      {/* Keeps the trigger's footprint in the layout while the panel floats. */}\n      <span aria-hidden className=\"invisible inline-flex h-9 items-center gap-2 px-3.5 text-[0.8125rem] font-medium [&_svg]:size-4\">\n        {icon}\n        {label}\n      </span>\n      <motion.div\n        layout\n        transition={t}\n        style={{ borderRadius: open ? 16 : 10 }}\n        className={cn(\n          \"absolute z-40 overflow-hidden border bg-raised text-fg\",\n          open ? \"border-border shadow-overlay\" : \"border-border shadow-key\",\n          POS[anchor]\n        )}\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {open ? (\n            <motion.div\n              key=\"panel\"\n              layout=\"position\"\n              initial={reduce ? false : { opacity: 0, filter: \"blur(6px)\", scale: 0.98 }}\n              animate={{ opacity: 1, filter: \"blur(0px)\", scale: 1 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, filter: \"blur(6px)\", transition: { duration: 0.12 } }}\n              transition={reduce ? { duration: 0 } : { ...SPRING, opacity: { duration: 0.2, delay: 0.06 } }}\n              style={{ width }}\n              role=\"dialog\"\n              aria-label={typeof label === \"string\" ? label : \"Panel\"}\n            >\n              {children ? children({ close }) : <FeedbackForm onSubmit={onSubmit} close={close} />}\n            </motion.div>\n          ) : (\n            <motion.button\n              key=\"trigger\"\n              ref={triggerRef}\n              layout=\"position\"\n              type=\"button\"\n              onClick={() => setOpen(true)}\n              aria-expanded={false}\n              initial={reduce ? false : { opacity: 0, filter: \"blur(4px)\" }}\n              animate={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, filter: \"blur(4px)\", transition: { duration: 0.1 } }}\n              transition={{ duration: 0.18 }}\n              className=\"inline-flex h-9 items-center gap-2 px-3.5 text-[0.8125rem] font-medium whitespace-nowrap outline-none transition-colors duration-[70ms] hover:bg-fg/[0.035] focus-visible:bg-fg/[0.05] [&_svg]:size-4 [&_svg]:text-fg-muted\"\n            >\n              {icon}\n              {label}\n            </motion.button>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  )\n}\n\nfunction FeedbackForm({ onSubmit, close }: { onSubmit?: (text: string) => void | Promise<void>; close: () => void }) {\n  const [text, setText] = React.useState(\"\")\n  const [phase, setPhase] = React.useState<\"idle\" | \"sending\" | \"sent\">(\"idle\")\n  const ref = React.useRef<HTMLTextAreaElement>(null)\n  React.useEffect(() => ref.current?.focus(), [])\n\n  const send = async () => {\n    if (!text.trim() || phase !== \"idle\") return\n    setPhase(\"sending\")\n    try {\n      await (onSubmit?.(text) ?? new Promise((r) => setTimeout(r, 700)))\n      setPhase(\"sent\")\n      setTimeout(close, 1500)\n    } catch {\n      setPhase(\"idle\")\n    }\n  }\n\n  if (phase === \"sent\") {\n    return (\n      <div className=\"grid h-[188px] place-items-center p-6 text-center\" aria-live=\"polite\">\n        <div>\n          <span className=\"mx-auto grid size-9 animate-[pop-in_320ms_var(--ease-hairline)_both] place-items-center rounded-full bg-success text-white shadow-ink\">\n            <CheckIcon className=\"size-4\" strokeWidth={2.5} />\n          </span>\n          <p className=\"mt-3 text-sm font-medium text-fg\">Sent. Thank you.</p>\n          <p className=\"mt-1 text-xs text-fg-muted\">We read every note, usually the same day.</p>\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault()\n        send()\n      }}\n    >\n      <div className=\"flex items-center justify-between px-4 pt-3.5\">\n        <p className=\"text-[0.8125rem] font-medium\">Send feedback</p>\n        <span className=\"font-mono text-[10px] text-fg-subtle tabular-nums\">{text.length}/500</span>\n      </div>\n      <textarea\n        ref={ref}\n        value={text}\n        maxLength={500}\n        onChange={(e) => setText(e.target.value)}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n            e.preventDefault()\n            send()\n          }\n        }}\n        placeholder=\"What would make Lumen better for you?\"\n        aria-label=\"Feedback\"\n        className=\"mt-2 block h-24 w-full resize-none bg-transparent px-4 text-sm leading-[1.55] text-fg outline-none placeholder:text-fg-subtle\"\n      />\n      <div className=\"flex items-center justify-between gap-2 border-t border-border bg-sunken/60 px-2 py-2\">\n        <button\n          type=\"button\"\n          onClick={close}\n          className=\"h-7 rounded-md px-2.5 text-xs font-medium text-fg-muted outline-none hover:bg-fg/5 hover:text-fg focus-visible:ring-2 focus-visible:ring-accent\"\n        >\n          Cancel\n        </button>\n        <button\n          type=\"submit\"\n          disabled={!text.trim() || phase === \"sending\"}\n          className=\"inline-flex h-7 items-center gap-2 rounded-md bg-ink pr-1.5 pl-2.5 text-xs font-medium text-on-ink shadow-ink outline-none transition-[background-color,opacity] duration-[70ms] hover:bg-ink-hover focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sunken disabled:opacity-50\"\n        >\n          {phase === \"sending\" ? \"Sending…\" : \"Send\"}\n          <kbd className=\"rounded-[4px] bg-on-ink/15 px-1 font-mono text-[10px] leading-4\">⌘↵</kbd>\n        </button>\n      </div>\n    </form>\n  )\n}\n\nexport { MorphPanel }\nexport type { MorphPanelProps }\n"
    }
  ]
}
