{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "input-group", "title": "Input Group", "description": "Groups an input with adjacent buttons, icons, or text add-ons for combined input actions.", "registryDependencies": [ "button" ], "files": [ { "path": "packages/ui/src/input-group.tsx", "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { Button } from \"./button\";\nimport { Input } from \"./input\";\nimport { Textarea } from \"./textarea\";\nimport { cn } from \"./utils\";\n\n// =============================================================================\n// InputGroupContext\n// =============================================================================\ntype InputGroupSize = \"sm\" | \"default\";\n\nconst InputGroupContext = React.createContext<{ size: InputGroupSize }>({\n size: \"default\",\n});\n\n// =============================================================================\n// InputGroup\n// =============================================================================\nexport interface InputGroupProps extends React.HTMLAttributes {\n /**\n * The children of the input group\n */\n children: React.ReactNode;\n /**\n * Optional additional className for the input group\n */\n className?: string;\n /**\n * Size variant of the input group\n * @default \"default\"\n */\n size?: \"sm\" | \"default\";\n}\n\nexport const InputGroup = ({\n className,\n children,\n size = \"default\",\n ref,\n ...props\n}: InputGroupProps & React.RefAttributes) => {\n const contextValue = React.useMemo(() => ({ size }), [size]);\n return (\n \n textarea]:h-auto has-[>textarea]:items-end\",\n \"has-[>textarea]:[&_[data-slot=input-group-addon]]:pb-2\",\n\n // Alignment variants - adjust input padding based on addon position\n \"has-[>[data-align=inline-start]]:[&_[data-slot=input-group-control]]:pl-1.5\",\n \"has-[>[data-align=inline-end]]:[&_[data-slot=input-group-control]]:pr-1.5\",\n \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&_[data-slot=input-group-control]]:pb-3\",\n \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&_[data-slot=input-group-control]]:pt-3\",\n\n // Focus state\n \"has-[[data-slot=input-group-control]:focus]:border-ring has-[[data-slot=input-group-control]:focus]:ring-1 has-[[data-slot=input-group-control]:focus]:ring-ring has-[[data-slot=input-group-control]:focus]:ring-inset\",\n\n // Error state\n \"has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-1 has-[[data-slot][aria-invalid=true]]:ring-destructive has-[[data-slot][aria-invalid=true]]:ring-inset\",\n\n className\n )}\n data-input=\"\"\n data-size={size}\n data-slot=\"input-group\"\n ref={ref}\n role=\"group\"\n {...props}\n >\n {children}\n \n \n );\n};\nInputGroup.displayName = \"InputGroup\";\n\n// =============================================================================\n// InputGroupAddon\n// =============================================================================\nconst inputGroupAddonVariants = cva(\n \"group/input-group-addon flex h-auto cursor-text select-none items-center justify-center gap-1 py-1.5 text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n align: {\n \"inline-start\": \"order-first pl-3 has-[>button]:-ml-2\",\n \"inline-end\": \"order-last pr-3 has-[>button]:-mr-2\",\n \"block-start\":\n \"order-first w-full justify-start px-3 pt-3 group-has-[[data-slot=input-group-control]]/input-group:pt-2.5\",\n \"block-end\":\n \"order-last w-full justify-start px-3 pb-3 group-has-[[data-slot=input-group-control]]/input-group:pb-2.5\",\n },\n },\n defaultVariants: {\n align: \"inline-start\",\n },\n }\n);\n\nexport interface InputGroupAddonProps\n extends React.HTMLAttributes,\n VariantProps {\n /**\n * The children of the addon\n */\n children: React.ReactNode;\n}\n\nexport const InputGroupAddon = ({\n className,\n align = \"inline-start\",\n children,\n ref,\n ...props\n}: InputGroupAddonProps & React.RefAttributes) => (\n {\n if ((e.target as HTMLElement).closest(\"button, select\")) {\n return;\n }\n e.currentTarget.parentElement\n ?.querySelector(\"[data-slot=input-group-control]\")\n ?.focus();\n }}\n ref={ref}\n role=\"group\"\n {...props}\n >\n {children}\n \n);\nInputGroupAddon.displayName = \"InputGroupAddon\";\n\n// =============================================================================\n// InputGroupButton\n// =============================================================================\nconst inputGroupButtonVariants = cva(\n \"flex items-center gap-2 text-sm shadow-none\",\n {\n variants: {\n size: {\n // Auto-fit the parent InputGroup with a 4px inset all around (one\n // tier smaller than the group, anchored to the input frame via the\n // addon's negative margin). Block addons: 32px toolbar height.\n // Icon-only when the only direct child is an svg.\n auto: [\n \"rounded-[calc(var(--radius)-5px)]\",\n \"[&>svg:not([class*='size-'])]:size-4\",\n // Default group (40px) — 32px button (4px inset)\n \"group-data-[size=default]/input-group:h-8\",\n \"group-data-[size=default]/input-group:gap-1.5\",\n \"group-data-[size=default]/input-group:px-2.5\",\n // Sm group (32px) — 24px button (4px inset), 14px icon (matches xs)\n \"group-data-[size=sm]/input-group:h-6\",\n \"group-data-[size=sm]/input-group:gap-1\",\n \"group-data-[size=sm]/input-group:px-2\",\n \"group-data-[size=sm]/input-group:[&>svg:not([class*='size-'])]:size-3.5\",\n // Icon-only (only direct child is svg): square + p-0\n \"has-[>svg:only-child]:p-0\",\n \"group-data-[size=default]/input-group:has-[>svg:only-child]:size-8\",\n \"group-data-[size=sm]/input-group:has-[>svg:only-child]:size-6\",\n // Block addons: 32px toolbar height regardless of group size\n \"group-data-[align=block-start]/input-group-addon:h-8\",\n \"group-data-[align=block-end]/input-group-addon:h-8\",\n \"group-data-[align=block-start]/input-group-addon:px-2.5\",\n \"group-data-[align=block-end]/input-group-addon:px-2.5\",\n \"group-data-[align=block-start]/input-group-addon:has-[>svg:only-child]:size-8\",\n \"group-data-[align=block-end]/input-group-addon:has-[>svg:only-child]:size-8\",\n // Multi-button addon (sibling buttons present): compact 24px chips\n // so two/three buttons fit cleanly without extending past the input\n // frame. Stays out of block-addon scope (toolbars handle their own sizing).\n \"group-has-[>button~button]/input-group-addon:h-6\",\n \"group-has-[>button~button]/input-group-addon:gap-1\",\n \"group-has-[>button~button]/input-group-addon:px-2\",\n \"group-has-[>button~button]/input-group-addon:[&>svg:not([class*='size-'])]:size-3.5\",\n \"group-has-[>button~button]/input-group-addon:has-[>svg:only-child]:size-6\",\n ].join(\" \"),\n xs: \"h-6 gap-1 rounded-sm px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5\",\n sm: \"h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5\",\n \"icon-xs\": \"size-6 rounded-sm p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 rounded-md p-0 has-[>svg]:p-0\",\n },\n },\n defaultVariants: {\n size: \"auto\",\n },\n }\n);\n\nexport interface InputGroupButtonProps\n extends Omit, \"size\">,\n VariantProps {\n /**\n * The type of button\n * @default \"button\"\n */\n type?: \"button\" | \"submit\" | \"reset\";\n /**\n * The variant of the button\n * @default \"ghost\"\n */\n variant?:\n | \"default\"\n | \"destructive\"\n | \"outline\"\n | \"secondary\"\n | \"ghost\"\n | \"link\";\n}\n\nexport const InputGroupButton = ({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"auto\",\n ref,\n ...props\n}: InputGroupButtonProps & React.RefAttributes) => (\n \n);\nInputGroupButton.displayName = \"InputGroupButton\";\n\n// =============================================================================\n// InputGroupText\n// =============================================================================\nexport interface InputGroupTextProps\n extends React.HTMLAttributes {}\n\nexport const InputGroupText = ({\n className,\n ref,\n ...props\n}: InputGroupTextProps & React.RefAttributes) => (\n \n);\nInputGroupText.displayName = \"InputGroupText\";\n\n// =============================================================================\n// InputGroupInput\n// =============================================================================\nexport interface InputGroupInputProps\n extends React.ComponentProps {}\n\nexport const InputGroupInput = ({\n className,\n size,\n ref,\n ...props\n}: InputGroupInputProps & React.RefAttributes) => {\n const { size: groupSize } = React.useContext(InputGroupContext);\n return (\n \n );\n};\nInputGroupInput.displayName = \"InputGroupInput\";\n\n// =============================================================================\n// InputGroupTextarea\n// =============================================================================\nexport interface InputGroupTextareaProps\n extends React.ComponentProps {}\n\nexport const InputGroupTextarea = ({\n className,\n ref,\n ...props\n}: InputGroupTextareaProps & React.RefAttributes) => (\n \n);\nInputGroupTextarea.displayName = \"InputGroupTextarea\";\n\n// Export variants for external use\nexport { inputGroupAddonVariants, inputGroupButtonVariants };\n", "type": "registry:ui" } ], "type": "registry:ui" }