1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import type * as React from "react";
5
6import { cn } from "~/lib/utils";
7import { Input, type InputProps } from "~/components/ui/input";
8
9const inputGroupVariants = cva(
10 "relative inline-flex w-full min-w-0 items-center rounded-[var(--control-radius)] border text-base text-foreground ring-ring/24 transition-shadow has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] has-[input:focus-visible,textarea:focus-visible]:ring-inset sm:text-sm dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--control-radius)-1px)]",
11 {
12 defaultVariants: {
13 variant: "default",
14 size: "default",
15 },
16 variants: {
17 size: {
18 default: null,
19 lg: "h-9",
20 },
21 variant: {
22 default:
23 "border-input bg-background not-dark:bg-clip-padding shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--control-radius)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)]",
24 // Matches floating glass cards (no shadow); focus darkens the hairline instead of a ring.
25 popover:
26 "dropdown-glass rounded-2xl has-[input:focus-visible,textarea:focus-visible]:ring-0 has-[input:focus-visible,textarea:focus-visible]:border-[color-mix(in_srgb,var(--contrast-foreground)_18%,transparent)]",
27 ghost:
28 "border-transparent bg-transparent shadow-none hover:bg-muted/40 has-[input:focus-visible,textarea:focus-visible]:bg-background",
29 },
30 },
31 },
32);
33
34function InputGroup({
35 className,
36 variant,
37 size,
38 ...props
39}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupVariants>) {
40 return (
41 <div
42 className={cn(inputGroupVariants({ variant, size }), className)}
43 data-slot="input-group"
44 role="group"
45 {...props}
46 />
47 );
48}
49
50const inputGroupAddonVariants = cva(
51 "[&_svg]:-mx-0.5 flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
52 {
53 defaultVariants: {
54 align: "inline-start",
55 },
56 compoundVariants: [
57 // Pill groups curve too tightly for buttons pulled to the edge; keep them inset.
58 {
59 align: "inline-end",
60 inset: "pill",
61 className: "has-[>button]:me-0 pe-1 [[data-size=sm]+&]:pe-1 gap-0.5",
62 },
63 {
64 align: "inline-start",
65 inset: "pill",
66 className: "has-[>button]:ms-0 ps-1 [[data-size=sm]+&]:ps-1 gap-0.5",
67 },
68 ],
69 variants: {
70 inset: {
71 default: null,
72 pill: "[&_button]:rounded-full",
73 },
74 align: {
75 "block-end":
76 "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
77 "block-start":
78 "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
79 "inline-end":
80 "has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
81 "inline-start":
82 "has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
83 },
84 },
85 },
86);
87
88function InputGroupAddon({
89 className,
90 align = "inline-start",
91 inset,
92 ...props
93}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
94 return (
95 <div
96 className={cn(inputGroupAddonVariants({ align, inset }), className)}
97 data-align={align}
98 data-slot="input-group-addon"
99 onMouseDown={(e) => {
100 const target = e.target as HTMLElement;
101 const isInteractive = target.closest(
102 "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
103 );
104 if (isInteractive) return;
105 e.preventDefault();
106 const parent = e.currentTarget.parentElement;
107 const input = parent?.querySelector<HTMLInputElement | HTMLTextAreaElement>(
108 "input, textarea",
109 );
110 if (input && !parent?.querySelector("input:focus, textarea:focus")) {
111 input.focus();
112 }
113 }}
114 {...props}
115 />
116 );
117}
118
119function InputGroupInput({ className, ...props }: InputProps) {
120 return <Input className={className} unstyled {...props} />;
121}
122
123export { InputGroup, InputGroupAddon, InputGroupInput };