{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "editable-handle",
	"type": "registry:ui",
	"title": "Editable Handle",
	"description": "A React Flow component that allows you to dynamically add, edit, or remove input/output handles on your nodes.",
	"dependencies": ["@xyflow/react"],
	"registryDependencies": [
		"button",
		"input",
		"textarea",
		"popover",
		"@simple-ai/base-handle"
	],
	"files": [
		{
			"path": "./src/registry/ui/flow/editable-handle.tsx",
			"content": "\"use client\";\n\nimport type { HandleProps, Node } from \"@xyflow/react\";\nimport { useOnSelectionChange } from \"@xyflow/react\";\nimport { Edit2, Trash } from \"lucide-react\";\nimport React, { useCallback, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { BaseHandle } from \"@/registry/ui/flow/base-handle\";\n\ntype HandleEditorProps = {\n\tvariant: \"edit\" | \"create\";\n\tlabel?: string;\n\tdescription?: string;\n\tonSave: (newLabel: string, newDescription?: string) => boolean;\n\tonCancel?: () => void;\n\talign?: \"start\" | \"end\";\n\tchildren: React.ReactNode;\n\tshowDescription?: boolean;\n};\n\nconst EditableHandleDialog = ({\n\tvariant,\n\tlabel,\n\tdescription,\n\tonSave,\n\tonCancel,\n\talign = \"start\",\n\tchildren,\n\tshowDescription = false,\n}: HandleEditorProps) => {\n\tconst [isOpen, setIsOpen] = useState(false);\n\tconst [localLabel, setLocalLabel] = useState(label ?? \"\");\n\tconst [localDescription, setLocalDescription] = useState(description);\n\n\tconst handleSave = () => {\n\t\t// Trim and validate the label has no spaces\n\t\tconst trimmedLabel = localLabel.trim();\n\t\tif (trimmedLabel.includes(\" \")) {\n\t\t\talert(\"Label cannot contain spaces\");\n\t\t\treturn;\n\t\t}\n\t\tconst success = onSave(trimmedLabel, localDescription?.trim());\n\t\tif (success) {\n\t\t\tsetIsOpen(false);\n\t\t\tif (variant === \"create\") {\n\t\t\t\treset();\n\t\t\t}\n\t\t}\n\t};\n\n\tconst handleCancel = () => {\n\t\tsetIsOpen(false);\n\t\tif (variant === \"create\") {\n\t\t\treset();\n\t\t}\n\t\tonCancel?.();\n\t};\n\n\tconst reset = () => {\n\t\tsetLocalLabel(\"\");\n\t\tsetLocalDescription(\"\");\n\t};\n\n\treturn (\n\t\t<Popover open={isOpen} onOpenChange={setIsOpen}>\n\t\t\t<PopoverTrigger asChild>{children}</PopoverTrigger>\n\t\t\t<PopoverContent className=\"w-80 p-4\" align={align}>\n\t\t\t\t<div className=\"flex flex-col gap-4\">\n\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t<label htmlFor=\"label\" className=\"text-sm font-medium\">\n\t\t\t\t\t\t\tLabel\n\t\t\t\t\t\t</label>\n\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\tid=\"label\"\n\t\t\t\t\t\t\tvalue={localLabel}\n\t\t\t\t\t\t\tonChange={(e) => setLocalLabel(e.target.value)}\n\t\t\t\t\t\t\tplaceholder=\"Enter label\"\n\t\t\t\t\t\t\tclassName=\"h-8\"\n\t\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t{showDescription && (\n\t\t\t\t\t\t<div className=\"flex flex-col gap-2\">\n\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\thtmlFor=\"description\"\n\t\t\t\t\t\t\t\tclassName=\"text-sm font-medium\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tDescription (optional)\n\t\t\t\t\t\t\t</label>\n\t\t\t\t\t\t\t<Textarea\n\t\t\t\t\t\t\t\tid=\"description\"\n\t\t\t\t\t\t\t\tvalue={localDescription}\n\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\tsetLocalDescription(e.target.value)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tplaceholder=\"Enter description\"\n\t\t\t\t\t\t\t\tclassName=\"resize-none h-20\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t<div className=\"flex justify-end gap-2\">\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tonClick={handleCancel}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tCancel\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button size=\"sm\" onClick={handleSave}>\n\t\t\t\t\t\t\t{variant === \"create\" ? \"Create\" : \"Save\"}\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</PopoverContent>\n\t\t</Popover>\n\t);\n};\n\nEditableHandleDialog.displayName = \"EditableHandleDialog\";\n\ntype EditableHandleProps = HandleProps &\n\tReact.HTMLAttributes<HTMLDivElement> & {\n\t\tnodeId: string;\n\t\thandleId: string;\n\t\tname: string;\n\t\tdescription?: string;\n\t\thandleClassName?: string;\n\t\tlabelClassName?: string;\n\t\twrapperClassName?: string;\n\t\tonUpdateTool: (\n\t\t\thandleId: string,\n\t\t\tnewName: string,\n\t\t\tnewDescription?: string,\n\t\t) => boolean;\n\t\tonDelete: (handleId: string) => void;\n\t\tshowDescription?: boolean;\n\t};\n\nconst EditableHandle = React.forwardRef<HTMLDivElement, EditableHandleProps>(\n\t(\n\t\t{\n\t\t\tnodeId,\n\t\t\thandleId,\n\t\t\tlabelClassName,\n\t\t\thandleClassName,\n\t\t\tname: label,\n\t\t\tdescription,\n\t\t\tposition,\n\t\t\twrapperClassName,\n\t\t\tonUpdateTool,\n\t\t\tonDelete,\n\t\t\tshowDescription = false,\n\t\t\t...handleProps\n\t\t},\n\t\tref,\n\t) => {\n\t\tconst [isEditing, setIsEditing] = useState(label.length === 0);\n\n\t\t// biome-ignore lint/correctness/useExhaustiveDependencies: Needed\n\t\tconst handleSelectionChange = useCallback(\n\t\t\t({ nodes }: { nodes: Node[] }) => {\n\t\t\t\tif (isEditing && !nodes.some((node) => node.id === nodeId)) {\n\t\t\t\t\tresetEditing();\n\t\t\t\t}\n\t\t\t},\n\t\t\t[isEditing, nodeId],\n\t\t);\n\n\t\tuseOnSelectionChange({\n\t\t\tonChange: handleSelectionChange,\n\t\t});\n\n\t\tconst resetEditing = () => {\n\t\t\tif (label.length === 0) {\n\t\t\t\tonDelete(handleId);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tsetIsEditing(false);\n\t\t};\n\n\t\tconst handleSave = (newLabel: string, newDescription?: string) => {\n\t\t\treturn onUpdateTool(handleId, newLabel, newDescription);\n\t\t};\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\ttitle={label}\n\t\t\t\tclassName={cn(\"relative group\", wrapperClassName)}\n\t\t\t>\n\t\t\t\t<BaseHandle\n\t\t\t\t\tposition={position}\n\t\t\t\t\tclassName={handleClassName}\n\t\t\t\t\tid={handleId}\n\t\t\t\t\t{...handleProps}\n\t\t\t\t/>\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\"flex items-center px-4 py-3 gap-3 nodrag\", {\n\t\t\t\t\t\t\"justify-end\": position === \"right\",\n\t\t\t\t\t\t\"justify-start\": position === \"left\",\n\t\t\t\t\t})}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"flex flex-col min-w-0\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"text-foreground truncate whitespace-nowrap\",\n\t\t\t\t\t\t\t\tlabelClassName,\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{label}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{showDescription && description && (\n\t\t\t\t\t\t\t<p className=\"text-muted-foreground text-sm line-clamp-1\">\n\t\t\t\t\t\t\t\t{description}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t<EditableHandleDialog\n\t\t\t\t\t\t\tvariant=\"edit\"\n\t\t\t\t\t\t\tlabel={label}\n\t\t\t\t\t\t\tdescription={description}\n\t\t\t\t\t\t\tonSave={handleSave}\n\t\t\t\t\t\t\tonCancel={resetEditing}\n\t\t\t\t\t\t\talign={position === \"left\" ? \"start\" : \"end\"}\n\t\t\t\t\t\t\tshowDescription={showDescription}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\tclassName=\"size-4 [&_svg]:size-3.5\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Edit2 />\n\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t</EditableHandleDialog>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\tclassName=\"size-4 [&_svg]:size-3.5\"\n\t\t\t\t\t\t\tonClick={() => onDelete(handleId)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Trash />\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t},\n);\n\nEditableHandle.displayName = \"EditableHandle\";\n\nexport { EditableHandle, EditableHandleDialog };\n",
			"type": "registry:ui",
			"target": "components/flow/editable-handle.tsx"
		}
	]
}
