{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "native-morphing-button-shadcnui", "type": "registry:component", "title": "Native Morphing Button", "description": "Floating action button that morphs into a menu of actions.", "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-shadcn/src/components/native/native-morphing-button-shadcnui.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n AnimatePresence,\n LayoutGroup,\n motion,\n Transition,\n useReducedMotion,\n} from \"framer-motion\";\nimport { Plus, X } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport interface MorphingButtonAction {\n /**\n * Display label for the action.\n */\n label: string;\n /**\n * Icon to display alongside the label.\n */\n icon: React.ReactNode;\n /**\n * Callback when action is clicked.\n */\n onClick: () => void;\n}\n\nexport interface NativeMorphingButtonProps {\n /**\n * Array of actions to display in the expanded menu.\n */\n actions: MorphingButtonAction[];\n /**\n * Position of the FAB.\n * Default: 'bottom-right'\n */\n position?: \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\";\n /**\n * Whether to use fixed positioning.\n * Default: false (relative to container)\n */\n fixed?: boolean;\n /**\n * Custom icon when collapsed.\n */\n icon?: React.ReactNode;\n /**\n * Custom close icon when expanded.\n */\n closeIcon?: React.ReactNode;\n className?: string;\n}\n\nconst positionClasses = {\n \"bottom-right\": \"bottom-4 right-4\",\n \"bottom-left\": \"bottom-4 left-4\",\n \"top-right\": \"top-4 right-4\",\n \"top-left\": \"top-4 left-4\",\n};\n\nconst springTransition: Transition = {\n type: \"spring\",\n stiffness: 300,\n damping: 30,\n};\nconst reducedTransition: Transition = { duration: 0.1 };\n\nexport function NativeMorphingButton({\n actions,\n position = \"bottom-right\",\n fixed = false,\n icon,\n closeIcon,\n className,\n}: NativeMorphingButtonProps) {\n const [isExpanded, setIsExpanded] = useState(false);\n const shouldReduceMotion = useReducedMotion();\n const fabRef = useRef(null);\n const menuRef = useRef(null);\n const wasExpanded = useRef(false);\n\n const transition = shouldReduceMotion ? reducedTransition : springTransition;\n\n // Focus the first action on open, return focus to the FAB on close.\n useEffect(() => {\n if (isExpanded) {\n wasExpanded.current = true;\n menuRef.current\n ?.querySelector('[role=\"menuitem\"]')\n ?.focus();\n } else if (wasExpanded.current) {\n fabRef.current?.focus();\n }\n }, [isExpanded]);\n\n const handleMenuKeyDown = (e: React.KeyboardEvent) => {\n const items = Array.from(\n menuRef.current?.querySelectorAll(\n '[role=\"menuitem\"]'\n ) ?? []\n );\n if (!items.length) return;\n const currentIndex = items.indexOf(\n document.activeElement as HTMLButtonElement\n );\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n items[(currentIndex + 1) % items.length]?.focus();\n }\n if (e.key === \"ArrowUp\") {\n e.preventDefault();\n items[(currentIndex - 1 + items.length) % items.length]?.focus();\n }\n };\n\n return (\n {\n if (e.key === \"Escape\" && isExpanded) {\n setIsExpanded(false);\n }\n }}\n >\n \n \n {/* Main FAB Button */}\n setIsExpanded(!isExpanded)}\n className=\"absolute right-0 bottom-0 z-10 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-shadow hover:shadow-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n whileHover={shouldReduceMotion ? undefined : { scale: 1.05 }}\n whileTap={shouldReduceMotion ? undefined : { scale: 0.95 }}\n aria-label={isExpanded ? \"Close menu\" : \"Open menu\"}\n aria-expanded={isExpanded}\n aria-haspopup=\"menu\"\n >\n \n {isExpanded ? (\n \n {closeIcon ?? }\n \n ) : (\n \n {icon ?? }\n \n )}\n \n \n\n {/* Expanded Menu */}\n \n {isExpanded && (\n <>\n setIsExpanded(false)}\n />\n \n
\n {actions.map((action, index) => (\n {\n action.onClick();\n setIsExpanded(false);\n }}\n className=\"flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left text-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n role=\"menuitem\"\n >\n \n {action.icon}\n \n {action.label}\n \n ))}\n
\n \n \n )}\n
\n \n
\n \n );\n}\n", "type": "registry:component", "target": "components/uitripled/native-morphing-button-shadcnui.tsx" } ] }