{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"contact-section","type":"registry:ui","title":"Contact section","description":"A contact form beside the ways to reach a team.","dependencies":["@radix-ui/react-popover","@radix-ui/react-slot","clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"path":"ui/alert.tsx","type":"registry:ui","target":"components/ui/alert.tsx","content":"\"use client\";\n\nimport { X } from \"lucide-react\";\nimport { useId, type ComponentProps, type ReactNode } from \"react\";\nimport { Button } from \"./button\";\nimport { FeedbackIcon, type FeedbackVariant } from \"./feedback\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/alert.css\";\nimport \"./alert.css\";\n\nexport type AlertProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n  title: ReactNode;\n  variant?: FeedbackVariant;\n  loading?: boolean;\n  action?: ReactNode;\n  onDismiss?: () => void;\n};\n\nexport function Alert({\n  title,\n  variant = \"neutral\",\n  loading = false,\n  children,\n  action,\n  onDismiss,\n  className,\n  ...props\n}: AlertProps) {\n  const titleId = useId();\n  return (\n    <div\n      data-slot=\"alert\"\n      data-variant={loading ? \"neutral\" : variant}\n      role={!loading && variant === \"error\" ? \"alert\" : \"status\"}\n      aria-busy={loading || undefined}\n      aria-atomic=\"true\"\n      aria-labelledby={titleId}\n      className={cn(\n        \"grid min-w-0 grid-cols-[2rem_minmax(0,1fr)_auto] items-start gap-3 rounded-overlay border border-border bg-surface p-4 text-sm text-foreground [overflow-wrap:anywhere]\",\n        className,\n      )}\n      {...props}\n    >\n      <FeedbackIcon variant={variant} loading={loading} />\n      <div data-slot=\"feedback-copy\" className=\"min-w-0 pt-1\">\n        <div id={titleId} data-slot=\"feedback-title\" className=\"font-semibold leading-5\">\n          <span key={loading ? \"loading\" : variant} data-slot=\"feedback-message\">{title}</span>\n        </div>\n        {children != null && (\n          <div data-slot=\"feedback-description\" className=\"mt-1 text-sm leading-5 text-muted-foreground\">\n            <span key={loading ? \"loading\" : variant} data-slot=\"feedback-message\">{children}</span>\n          </div>\n        )}\n        {action != null && (\n          <div data-slot=\"feedback-action\" className=\"mt-3 flex max-w-full flex-wrap items-center gap-2\">\n            {action}\n          </div>\n        )}\n      </div>\n      {onDismiss && (\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"-mt-1 -mr-1\"\n          aria-label=\"Dismiss alert\"\n          onClick={onDismiss}\n        >\n          <X aria-hidden=\"true\" />\n        </Button>\n      )}\n    </div>\n  );\n}\n"},{"path":"ui/button.tsx","type":"registry:ui","target":"components/ui/button.tsx","content":"\"use client\";\n\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { LoaderCircle } from \"lucide-react\";\nimport {\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/loading-indicator.css\";\nimport \"./button.css\";\n\n/* Colour, material and press live in button.css keyed on data-variant, so a\n   consumer's utility classes still override them. Raised to press: the\n   button sinks 1px into an inner shade instead of scaling. */\nconst variants = {\n  default: \"\",\n  secondary: \"\",\n  ghost: \"\",\n  destructive: \"\",\n};\n\nconst sizes = {\n  sm: \"h-8 gap-1.5 rounded-full px-3 text-[13px] max-md:min-h-11 [@media(pointer:coarse)]:min-h-11\",\n  md: \"h-[var(--control-height)] gap-2 rounded-full px-3.5 text-sm\",\n  lg: \"h-11 gap-2 rounded-full px-[18px] text-[15px]\",\n  icon: \"size-[var(--control-height)] rounded-full p-0\",\n};\n\nexport type ButtonProps = ComponentProps<\"button\"> & {\n  variant?: keyof typeof variants;\n  size?: keyof typeof sizes;\n  asChild?: boolean;\n  /** Shows a spinner over the label and swallows clicks while keeping focus and width. */\n  loading?: boolean;\n  /** Animate a meaningful label/icon change, such as saving → saved. */\n  feedbackKey?: string;\n};\n\nexport function Button({\n  className,\n  variant = \"default\",\n  size = \"md\",\n  asChild = false,\n  loading = false,\n  feedbackKey,\n  disabled,\n  \"aria-disabled\": ariaDisabled,\n  children,\n  onClick,\n  onClickCapture,\n  tabIndex,\n  type = \"button\",\n  ...props\n}: ButtonProps) {\n  const Component = asChild ? Slot : \"button\";\n  const nativeDisabled = Boolean(disabled);\n  const blocked =\n    nativeDisabled ||\n    loading ||\n    ariaDisabled === true ||\n    ariaDisabled === \"true\";\n\n  return (\n    <Component\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      type={asChild ? undefined : type}\n      className={cn(\n        \"relative inline-flex shrink-0 items-center justify-center whitespace-nowrap border border-transparent font-medium tracking-[-0.006em] outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none aria-disabled:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n        variants[variant],\n        sizes[size],\n        className,\n      )}\n      disabled={asChild ? undefined : nativeDisabled}\n      aria-disabled={nativeDisabled || ariaDisabled ? true : undefined}\n      aria-busy={loading || undefined}\n      tabIndex={asChild && nativeDisabled ? -1 : tabIndex}\n      {...props}\n      onKeyDown={(event) => {\n        if (blocked && (event.key === \"Enter\" || event.key === \" \")) {\n          event.preventDefault();\n        }\n        props.onKeyDown?.(event);\n      }}\n      onClickCapture={(event) => {\n        if (blocked) {\n          event.preventDefault();\n          event.stopPropagation();\n          return;\n        }\n        onClickCapture?.(event);\n      }}\n      onClick={onClick}\n    >\n      {loading && asChild && (\n        <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />\n      )}\n      {loading && !asChild && feedbackKey === undefined && (\n        <span data-slot=\"button-busy\" aria-hidden=\"true\">\n          <LoaderCircle data-slot=\"loading-indicator\" />\n        </span>\n      )}\n      <Slottable>\n        {loading && !asChild && feedbackKey === undefined ? (\n          <span data-slot=\"button-label\" data-hidden=\"true\">\n            {children}\n          </span>\n        ) : feedbackKey !== undefined && !asChild ? (\n          <ButtonFeedback stateKey={feedbackKey}>\n            {loading && (\n              <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />\n            )}\n            {children}\n          </ButtonFeedback>\n        ) : (\n          children\n        )}\n      </Slottable>\n    </Component>\n  );\n}\n\nfunction ButtonFeedback({\n  stateKey,\n  children,\n}: {\n  stateKey: string;\n  children: ReactNode;\n}) {\n  const content = useRef<HTMLSpanElement>(null);\n  const [initialKey] = useState(stateKey);\n  const [width, setWidth] = useState<number>();\n  useLayoutEffect(() => {\n    const element = content.current;\n    if (!element) return;\n    const measure = () => setWidth(element.offsetWidth);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [stateKey]);\n  return (\n    <span data-slot=\"button-feedback\" style={{ width }}>\n      <span\n        key={stateKey}\n        ref={content}\n        data-slot=\"button-feedback-label\"\n        data-enter={stateKey !== initialKey}\n      >\n        {children}\n      </span>\n    </span>\n  );\n}\n"},{"path":"ui/contact-section.tsx","type":"registry:ui","target":"components/ui/contact-section.tsx","content":"\"use client\";\nimport { CheckCircle2 } from \"lucide-react\";\nimport { useEffect, useId, useRef, useState, type FormEvent } from \"react\";\nimport { Alert } from \"./alert\";\nimport { Button } from \"./button\";\nimport { Field, isEmail } from \"./field\";\nimport { Input } from \"./input\";\nimport { cn } from \"./lib/cn\";\nimport { SelectMenu } from \"./select-menu\";\nimport { Textarea } from \"./textarea\";\n\nexport type ContactDetail = { label: string; value: string; href?: string };\nexport type ContactResult = void | { error?: string; fields?: { name?: string; email?: string; message?: string } };\n\n/** A contact form beside a few direct ways to reach you. It sends nothing itself and keeps the message if sending fails. */\nexport function ContactSection({\n  title = \"Contact us\",\n  description,\n  details,\n  topics,\n  onSubmit,\n  successTitle = \"Message sent\",\n  successText = \"Thanks. We will reply within one business day.\",\n  className,\n}: {\n  title?: string;\n  description?: string;\n  details?: readonly ContactDetail[];\n  topics?: readonly string[];\n  onSubmit: (values: { name: string; email: string; topic: string; message: string }) => Promise<ContactResult> | ContactResult;\n  successTitle?: string;\n  successText?: string;\n  className?: string;\n}) {\n  const [values, setValues] = useState({ name: \"\", email: \"\", topic: topics?.[0] ?? \"\", message: \"\", website: \"\" });\n  const [touched, setTouched] = useState({ name: false, email: false, message: false });\n  const [server, setServer] = useState<{ error?: string; fields?: { name?: string; email?: string; message?: string } }>({});\n  const [pending, setPending] = useState(false),\n    [sent, setSent] = useState(false);\n  const form = useRef<HTMLFormElement>(null);\n  const refocus = useRef<string | null>(null);\n  const heading = useId();\n  const thanks = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (pending || !refocus.current) return;\n    form.current?.querySelector<HTMLElement>(`[name=\"${refocus.current}\"]`)?.focus();\n    refocus.current = null;\n  }, [pending]);\n  useEffect(() => {\n    if (sent) thanks.current?.focus();\n  }, [sent]);\n\n  const local = {\n    name: !values.name.trim() ? \"Tell us your name.\" : \"\",\n    email: !values.email.trim() ? \"Enter your email address.\" : !isEmail(values.email) ? \"Enter a valid email address, like name@example.com.\" : \"\",\n    message: values.message.trim().length < 10 ? \"Write a few words about what you need (at least 10 characters).\" : \"\",\n  };\n  const errors = {\n    name: (touched.name && local.name) || server.fields?.name || \"\",\n    email: (touched.email && local.email) || server.fields?.email || \"\",\n    message: (touched.message && local.message) || server.fields?.message || \"\",\n  };\n\n  const submit = async (event: FormEvent) => {\n    event.preventDefault();\n    if (pending) return;\n    setTouched({ name: true, email: true, message: true });\n    setServer({});\n    const first = ([\"name\", \"email\", \"message\"] as const).find((k) => local[k]);\n    if (first) {\n      form.current?.querySelector<HTMLElement>(`[name=\"${first}\"]`)?.focus();\n      return;\n    }\n    // Real people never see the hidden field; if it has text, pretend it worked.\n    if (values.website) {\n      setSent(true);\n      return;\n    }\n    setPending(true);\n    let target: string | undefined;\n    try {\n      const result = await onSubmit({ name: values.name.trim(), email: values.email.trim(), topic: values.topic, message: values.message.trim() });\n      if (result && (result.error || result.fields)) {\n        setServer(result);\n        target = ([\"name\", \"email\", \"message\"] as const).find((k) => result.fields?.[k]);\n      } else setSent(true);\n    } catch (error) {\n      setServer({ error: error instanceof Error ? error.message : \"Something went wrong. Your message is still here; try again.\" });\n    } finally {\n      setPending(false);\n    }\n    if (target) refocus.current = target;\n  };\n\n  return (\n    <section data-slot=\"contact\" aria-labelledby={heading} className={cn(\"mx-auto grid w-full max-w-5xl gap-10 px-4 py-12 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.25fr)]\", className)}>\n      <div className=\"grid content-start gap-4\">\n        <h2 id={heading} className=\"text-2xl font-semibold tracking-tight\">\n          {title}\n        </h2>\n        {description && <p className=\"max-w-sm text-sm text-muted-foreground\">{description}</p>}\n        {details && details.length > 0 && (\n          <dl className=\"mt-2 grid gap-4 text-sm\">\n            {details.map((d) => (\n              <div key={d.label}>\n                <dt className=\"text-xs text-muted-foreground\">{d.label}</dt>\n                <dd className=\"mt-0.5\">\n                  {d.href ? (\n                    <a href={d.href} className=\"rounded-control underline underline-offset-4 outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\">\n                      {d.value}\n                    </a>\n                  ) : (\n                    d.value\n                  )}\n                </dd>\n              </div>\n            ))}\n          </dl>\n        )}\n      </div>\n      <div className=\"rounded-overlay border border-border bg-background p-5 shadow-resting sm:p-6\">\n        {sent ? (\n          <div ref={thanks} tabIndex={-1} role=\"status\" className=\"grid justify-items-start gap-3 outline-none\">\n            <CheckCircle2 aria-hidden=\"true\" className=\"size-6 text-success\" />\n            <h3 className=\"text-lg font-semibold\">{successTitle}</h3>\n            <p className=\"text-sm text-muted-foreground\">{successText}</p>\n            <Button\n              variant=\"secondary\"\n              size=\"sm\"\n              onClick={() => {\n                setSent(false);\n                setValues({ name: \"\", email: \"\", topic: topics?.[0] ?? \"\", message: \"\", website: \"\" });\n                setTouched({ name: false, email: false, message: false });\n              }}\n            >\n              Send another message\n            </Button>\n          </div>\n        ) : (\n          <form ref={form} noValidate aria-labelledby={heading} onSubmit={submit} className=\"grid gap-4\">\n            {server.error && <Alert variant=\"error\" title={server.error} />}\n            <div className=\"grid gap-4 sm:grid-cols-2\">\n              <Field label=\"Name\" error={errors.name} required>\n                {(control) => <Input {...control} name=\"name\" autoComplete=\"name\" value={values.name} disabled={pending} onChange={(e) => setValues({ ...values, name: e.target.value })} onBlur={() => setTouched((t) => ({ ...t, name: true }))} />}\n              </Field>\n              <Field label=\"Email\" error={errors.email} required>\n                {(control) => (\n                  <Input {...control} name=\"email\" type=\"email\" inputMode=\"email\" autoComplete=\"email\" autoCapitalize=\"none\" spellCheck={false} value={values.email} disabled={pending} onChange={(e) => setValues({ ...values, email: e.target.value })} onBlur={() => setTouched((t) => ({ ...t, email: true }))} />\n                )}\n              </Field>\n            </div>\n            {topics && topics.length > 0 && (\n              <Field label=\"Topic\">\n                {(control) => (\n                  <SelectMenu {...control} name=\"topic\" value={values.topic} disabled={pending} onValueChange={(topic) => setValues({ ...values, topic })} options={topics.map((t) => ({ value: t, label: t }))} />\n                )}\n              </Field>\n            )}\n            <Field label=\"Message\" error={errors.message} required description={`${values.message.trim().length} characters`}>\n              {(control) => <Textarea {...control} name=\"message\" rows={5} value={values.message} disabled={pending} onChange={(e) => setValues({ ...values, message: e.target.value })} onBlur={() => setTouched((t) => ({ ...t, message: true }))} />}\n            </Field>\n            {/* Hidden from people; automated forms fill it in. */}\n            <div aria-hidden=\"true\" className=\"absolute -left-[9999px] h-0 w-0 overflow-hidden\">\n              <label>\n                Website\n                <input name=\"website\" tabIndex={-1} autoComplete=\"off\" value={values.website} onChange={(e) => setValues({ ...values, website: e.target.value })} />\n              </label>\n            </div>\n            <Button type=\"submit\" loading={pending} className=\"justify-self-start\">\n              {pending ? \"Sending…\" : \"Send message\"}\n            </Button>\n          </form>\n        )}\n      </div>\n    </section>\n  );\n}\n"},{"path":"ui/feedback.tsx","type":"registry:ui","target":"components/ui/feedback.tsx","content":"import { CircleCheck, CircleX, Info, LoaderCircle, TriangleAlert } from \"lucide-react\";\nimport \"./feedback.css\";\n\nexport type FeedbackVariant = \"neutral\" | \"success\" | \"warning\" | \"error\";\n\nconst icons = {\n  neutral: Info,\n  success: CircleCheck,\n  warning: TriangleAlert,\n  error: CircleX,\n};\n\nexport function FeedbackIcon({\n  variant,\n  loading = false,\n}: {\n  variant: FeedbackVariant;\n  loading?: boolean;\n}) {\n  const Icon = loading ? LoaderCircle : icons[variant];\n  return (\n    <span data-slot=\"feedback-icon\" aria-hidden=\"true\">\n      <span data-slot=\"feedback-symbol\" key={loading ? \"loading\" : variant}>\n        <Icon data-slot={loading ? \"loading-indicator\" : undefined} />\n      </span>\n    </span>\n  );\n}\n"},{"path":"ui/field.tsx","type":"registry:ui","target":"components/ui/field.tsx","content":"\"use client\";\nimport { CircleAlert, CircleCheck, LoaderCircle } from \"lucide-react\";\nimport { useId, type ReactNode } from \"react\";\nimport { Label } from \"./label\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/field-message.css\";\nimport \"./shared/loading-indicator.css\";\nimport \"./field.css\";\n\nexport type FieldControlProps = {\n  id: string;\n  \"aria-describedby\"?: string;\n  \"aria-invalid\"?: true;\n  \"aria-required\"?: true;\n};\n\nexport const isEmail = (value: string) => /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value.trim());\n\n/**\n * A label, a control and one message line wired together for assistive\n * technology. Hint, async status and error share the same reserved line and\n * rewrite it in place, so validation never pushes the next field down.\n */\nexport function Field({\n  label,\n  description,\n  error,\n  status,\n  statusTone = \"pending\",\n  count,\n  required,\n  optional,\n  action,\n  className,\n  children,\n}: {\n  label: string;\n  /** Hint shown on the message line until a status or error replaces it. */\n  description?: ReactNode;\n  /** Replaces the hint and is announced when it appears. */\n  error?: string;\n  /** An async result such as “Checking availability…” or “Available”. */\n  status?: string;\n  statusTone?: \"pending\" | \"success\";\n  /** Right-aligned counter on the message line, e.g. “6 / 40”. */\n  count?: string;\n  required?: boolean;\n  /** Adds \"(optional)\" after the label. */\n  optional?: boolean;\n  /** Something aligned with the label, such as a \"Forgot password?\" link. */\n  action?: ReactNode;\n  className?: string;\n  children: (props: FieldControlProps) => ReactNode;\n}) {\n  const id = useId();\n  const message = `${id}-message`;\n  const hasMessage = Boolean(error || status || description || count);\n  const tone = error ? \"error\" : status ? statusTone : \"hint\";\n  return (\n    <div data-slot=\"field\" className={cn(\"grid gap-1.5\", className)}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <Label htmlFor={id}>\n          {label}\n          {optional && <span className=\"ml-1 font-normal text-muted-foreground\">(optional)</span>}\n        </Label>\n        {action}\n      </div>\n      {children({\n        id,\n        \"aria-describedby\": hasMessage ? message : undefined,\n        \"aria-invalid\": error ? true : undefined,\n        \"aria-required\": required ? true : undefined,\n      })}\n      <p id={message} data-slot=\"field-message\" data-tone={tone} aria-live={error ? \"assertive\" : \"polite\"} role={error ? \"alert\" : undefined}>\n        {tone === \"error\" && <CircleAlert aria-hidden=\"true\" />}\n        {tone === \"success\" && <CircleCheck aria-hidden=\"true\" />}\n        {tone === \"pending\" && <LoaderCircle data-slot=\"loading-indicator\" aria-hidden=\"true\" />}\n        <span data-slot=\"field-error\" className=\"min-w-0 flex-1\">{error ?? status ?? description}</span>\n        {count && <span data-slot=\"field-count\">{count}</span>}\n      </p>\n    </div>\n  );\n}\n"},{"path":"ui/input.tsx","type":"registry:ui","target":"components/ui/input.tsx","content":"import type { ComponentProps, ReactNode } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/input.css\";\nimport \"./shared/input-2.css\";\nimport \"./shared/input-group.css\";\nimport \"./input.css\";\n\nconst fieldText =\n  \"w-full min-w-0 text-base text-foreground outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm\";\n\nexport type InputProps = ComponentProps<\"input\"> & {\n  /** Fixed text or an icon before the value, e.g. \"caliper.app/\". */\n  leading?: ReactNode;\n  /** Text, an icon or a status after the value, e.g. a spinner or a count. */\n  trailing?: ReactNode;\n};\n\n/**\n * A recessed text field. Fields sit slightly below the paper (an inner shade\n * and a 3:1 ring); buttons stand on it, so what you fill and what you press\n * never look alike.\n */\nexport function Input({ className, leading, trailing, ...props }: InputProps) {\n  if (leading === undefined && trailing === undefined) {\n    return (\n      <input\n        data-slot=\"input\"\n        className={cn(\"flex h-[var(--control-height)] rounded-button px-3\", fieldText, className)}\n        {...props}\n      />\n    );\n  }\n  return (\n    <span\n      data-slot=\"input-group\"\n      data-disabled={props.disabled || undefined}\n      data-invalid={props[\"aria-invalid\"] === true || props[\"aria-invalid\"] === \"true\" || undefined}\n      data-readonly={props.readOnly || undefined}\n      className={cn(\"flex h-[var(--control-height)] w-full min-w-0 items-center gap-2 rounded-button px-3\", className)}\n    >\n      {leading !== undefined && <span data-slot=\"input-affix\">{leading}</span>}\n      <input data-slot=\"input-bare\" className={cn(\"h-full flex-1 bg-transparent\", fieldText)} {...props} />\n      {trailing !== undefined && <span data-slot=\"input-trailing\">{trailing}</span>}\n    </span>\n  );\n}\n"},{"path":"ui/label.tsx","type":"registry:ui","target":"components/ui/label.tsx","content":"import type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\n\nexport function Label({ className, ...props }: ComponentProps<\"label\">) {\n  return (\n    <label\n      data-slot=\"label\"\n      className={cn(\n        \"inline-block max-w-full text-sm leading-5 font-medium [overflow-wrap:anywhere]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"},{"path":"ui/lib/cn.ts","type":"registry:ui","target":"components/ui/lib/cn.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\nconst twMerge = extendTailwindMerge({\n  extend: { theme: { radius: [\"control\", \"surface\", \"overlay\"], shadow: [\"overlay\"] } },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"},{"path":"ui/select-menu.tsx","type":"registry:ui","target":"components/ui/select-menu.tsx","content":"\"use client\";\n\nimport * as Popover from \"@radix-ui/react-popover\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport {\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/select.css\";\nimport \"./shared/input-2.css\";\nimport \"./select-menu.css\";\n\nexport type SelectMenuOption = {\n  value: string;\n  label: string;\n  /** Short identifier shown in mono at the end of the row, e.g. \"eu-central-1\". */\n  code?: string;\n  /** A 16px mark before the label, e.g. a network dot or a person's initials. */\n  icon?: ReactNode;\n  disabled?: boolean;\n};\n\nconst ROW = 36;\nconst PAD = 4;\n// The list is a little wider than its trigger and starts before it, so the chosen row sits over the trigger.\nconst WIDER = 36;\nconst ALIGN = 28;\nconst EDGE = 12;\n\nfunction useCoarsePointer() {\n  const [coarse, setCoarse] = useState(false);\n  useEffect(() => {\n    const query = window.matchMedia(\"(pointer: coarse)\");\n    const update = () => setCoarse(query.matches);\n    update();\n    query.addEventListener(\"change\", update);\n    return () => query.removeEventListener(\"change\", update);\n  }, []);\n  return coarse;\n}\n\n/**\n * A select whose list opens with the chosen option lying exactly over the\n * trigger text, so the eye never jumps. On touch screens it is the platform\n * picker. A hidden input carries the value in native forms.\n */\nexport function SelectMenu({\n  options,\n  value: controlled,\n  defaultValue,\n  onValueChange,\n  placeholder = \"Choose…\",\n  name,\n  disabled,\n  size = \"md\",\n  variant = \"field\",\n  className,\n  ref,\n  ...aria\n}: {\n  options: readonly SelectMenuOption[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  placeholder?: string;\n  /** Submitted with the form. */\n  name?: string;\n  disabled?: boolean;\n  /** sm is 32px for toolbars, filters and table rows; md follows the control height. */\n  size?: \"sm\" | \"md\";\n  /** ghost drops the field ring, for a select inside a composed control (a currency in a money field). */\n  variant?: \"field\" | \"ghost\";\n  className?: string;\n  /** The trigger (or, on touch screens, the native select), e.g. for a dialog's first focus. */\n  ref?: Ref<HTMLButtonElement | HTMLSelectElement>;\n} & Pick<ComponentProps<\"button\">, \"id\" | \"aria-label\" | \"aria-labelledby\" | \"aria-describedby\" | \"aria-invalid\" | \"aria-required\">) {\n  const listId = useId();\n  const coarse = useCoarsePointer();\n  const [inner, setInner] = useState(defaultValue);\n  const value = controlled ?? inner;\n  const [open, setOpen] = useState(false);\n  const [active, setActive] = useState(0);\n  const [offset, setOffset] = useState(0);\n  // Keeps the list inside the dialog or card it opens in, when laying it over the trigger would cross the edge.\n  const [shift, setShift] = useState(0);\n  const [room, setRoom] = useState<number | undefined>(undefined);\n  const trigger = useRef<HTMLButtonElement>(null);\n  const list = useRef<HTMLDivElement>(null);\n  const selectedIndex = Math.max(0, options.findIndex((option) => option.value === value));\n  const selected = options.find((option) => option.value === value);\n\n  // If the change handler moves focus on (e.g. into a reply box), leave it there when the list closes.\n  const keepFocus = useRef(false);\n  // A form may disable the trigger while it saves the choice. Focus cannot return to a disabled button,\n  // so it returns as soon as the trigger is enabled again, unless the reader has moved on by then.\n  const refocus = useRef(false);\n  useEffect(() => {\n    if (disabled || !refocus.current) return;\n    refocus.current = false;\n    const now = document.activeElement;\n    if (!now || now === document.body) trigger.current?.focus();\n  }, [disabled]);\n  function choose(next: string) {\n    if (controlled === undefined) setInner(next);\n    onValueChange?.(next);\n    const now = document.activeElement;\n    keepFocus.current = !!now && now !== document.body && now !== trigger.current && !list.current?.contains(now);\n  }\n\n  function openList(nextOpen: boolean) {\n    if (nextOpen && trigger.current) {\n      // Lay the selected row over the trigger, unless that would leave the viewport.\n      const rect = trigger.current.getBoundingClientRect();\n      const above = PAD + selectedIndex * ROW;\n      const fits = rect.top - above > 8 && rect.top - above + options.length * ROW + PAD * 2 < window.innerHeight - 8;\n      setOffset(fits ? -(rect.height + above) - (ROW - rect.height) / 2 : 6);\n      const card = trigger.current.closest<HTMLElement>('[role=\"dialog\"]')?.getBoundingClientRect();\n      const left = rect.left - ALIGN;\n      const right = left + rect.width + WIDER;\n      setShift(!card ? 0 : left < card.left + EDGE ? card.left + EDGE - left : right > card.right - EDGE ? card.right - EDGE - right : 0);\n      setRoom(card ? card.width - EDGE * 2 : undefined);\n      setActive(selectedIndex);\n    }\n    setOpen(nextOpen);\n  }\n\n  function move(delta: number) {\n    let next = active;\n    for (let step = 0; step < options.length; step += 1) {\n      next = Math.min(options.length - 1, Math.max(0, next + delta));\n      if (!options[next].disabled) break;\n    }\n    setActive(next);\n  }\n\n  function onListKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n    if (event.key === \"ArrowDown\") move(1);\n    else if (event.key === \"ArrowUp\") move(-1);\n    else if (event.key === \"Home\") setActive(options.findIndex((option) => !option.disabled));\n    else if (event.key === \"End\") setActive(options.length - 1 - [...options].reverse().findIndex((option) => !option.disabled));\n    else if (event.key === \"Enter\" || event.key === \" \") {\n      const option = options[active];\n      if (option && !option.disabled) {\n        choose(option.value);\n        setOpen(false);\n      }\n    } else if (event.key === \"Tab\") setOpen(false);\n    else if (/^\\S$/.test(event.key)) {\n      const start = (active + 1) % options.length;\n      const order = [...options.slice(start), ...options.slice(0, start)];\n      const hit = order.find((option) => !option.disabled && option.label.toLowerCase().startsWith(event.key.toLowerCase()));\n      if (hit) setActive(options.indexOf(hit));\n    } else return;\n    event.preventDefault();\n  }\n\n  useEffect(() => {\n    if (!open) return;\n    list.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`)?.scrollIntoView({ block: \"nearest\" });\n  }, [active, open]);\n\n  if (coarse) {\n    return (\n      <span data-slot=\"select-menu-native\" data-size={size} data-variant={variant} className={cn(\"relative block\", className)}>\n        <select\n          ref={ref as Ref<HTMLSelectElement>}\n          data-slot=\"select\"\n          {...aria}\n          name={name}\n          disabled={disabled}\n          value={value ?? \"\"}\n          onChange={(event) => choose(event.target.value)}\n          className={cn(\n            \"block w-full min-w-0 appearance-none text-base text-foreground outline-none\",\n            size === \"sm\" ? \"h-8 rounded-control pr-8 pl-2.5\" : \"h-[var(--control-height)] rounded-button pr-9 pl-3\",\n          )}\n        >\n          {!selected && <option value=\"\" disabled>{placeholder}</option>}\n          {options.map((option) => (\n            <option key={option.value} value={option.value} disabled={option.disabled}>\n              {option.label}\n              {option.code ? ` · ${option.code}` : \"\"}\n            </option>\n          ))}\n        </select>\n        <ChevronsUpDown aria-hidden=\"true\" className={cn(\"pointer-events-none absolute top-1/2 size-4 -translate-y-1/2 text-muted-foreground\", size === \"sm\" ? \"right-2.5\" : \"right-3\")} />\n      </span>\n    );\n  }\n\n  return (\n    <Popover.Root open={open} onOpenChange={openList}>\n      <Popover.Trigger asChild>\n        <button\n          ref={(node) => {\n            trigger.current = node;\n            if (typeof ref === \"function\") ref(node);\n            else if (ref) (ref as { current: HTMLButtonElement | HTMLSelectElement | null }).current = node;\n          }}\n          type=\"button\"\n          data-slot=\"select-menu\"\n          data-size={size}\n          data-variant={variant}\n          role=\"combobox\"\n          aria-haspopup=\"listbox\"\n          aria-expanded={open}\n          aria-controls={open ? listId : undefined}\n          disabled={disabled}\n          {...aria}\n          onKeyDown={(event) => {\n            if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n              event.preventDefault();\n              openList(true);\n            }\n          }}\n          className={cn(\n            \"flex w-full min-w-0 items-center justify-between gap-2 text-left text-foreground outline-none disabled:cursor-not-allowed\",\n            size === \"sm\" ? \"h-8 rounded-control px-2.5 text-[13px]\" : \"h-[var(--control-height)] rounded-button px-3 text-base md:text-sm\",\n            className,\n          )}\n        >\n          <span className={cn(\"flex min-w-0 items-center gap-2\", !selected && \"text-muted-foreground\")}>\n            {selected?.icon && <span data-slot=\"select-menu-icon\" aria-hidden=\"true\">{selected.icon}</span>}\n            <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n            {selected?.code && <span data-slot=\"select-menu-code\" className=\"shrink-0\">{selected.code}</span>}\n          </span>\n          <ChevronsUpDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n        </button>\n      </Popover.Trigger>\n      {name && <input type=\"hidden\" name={name} value={value ?? \"\"} />}\n      <Popover.Portal>\n        <Popover.Content\n          side=\"bottom\"\n          align=\"start\"\n          sideOffset={offset}\n          alignOffset={shift - ALIGN}\n          avoidCollisions={offset > 0}\n          onOpenAutoFocus={(event) => {\n            event.preventDefault();\n            keepFocus.current = false;\n            list.current?.focus();\n          }}\n          onCloseAutoFocus={(event) => {\n            if (keepFocus.current) event.preventDefault();\n            else if (trigger.current?.disabled) {\n              event.preventDefault();\n              refocus.current = true;\n            }\n          }}\n          data-slot=\"select-menu-content\"\n          style={{ width: `calc(var(--radix-popover-trigger-width) + ${WIDER}px)`, maxWidth: room, transformOrigin: `50% ${PAD + selectedIndex * ROW + ROW / 2}px` }}\n        >\n          <div\n            ref={list}\n            id={listId}\n            role=\"listbox\"\n            tabIndex={-1}\n            aria-labelledby={aria[\"aria-labelledby\"]}\n            aria-label={aria[\"aria-label\"]}\n            aria-activedescendant={`${listId}-${active}`}\n            onKeyDown={onListKeyDown}\n            className=\"max-h-[calc(8*36px+8px)] overflow-y-auto p-1 outline-none\"\n          >\n            {options.map((option, index) => (\n              <div\n                key={option.value}\n                id={`${listId}-${index}`}\n                role=\"option\"\n                data-index={index}\n                aria-selected={option.value === value}\n                aria-disabled={option.disabled || undefined}\n                data-active={index === active}\n                data-slot=\"select-menu-option\"\n                onPointerMove={() => !option.disabled && setActive(index)}\n                onClick={() => {\n                  if (option.disabled) return;\n                  choose(option.value);\n                  setOpen(false);\n                }}\n              >\n                <span className=\"grid size-4 place-items-center\">{option.value === value && <Check aria-hidden=\"true\" className=\"size-4\" />}</span>\n                {option.icon && <span data-slot=\"select-menu-icon\" aria-hidden=\"true\">{option.icon}</span>}\n                <span className=\"min-w-0 flex-1 truncate\">{option.label}</span>\n                {option.code && <span data-slot=\"select-menu-code\">{option.code}</span>}\n              </div>\n            ))}\n          </div>\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n"},{"path":"ui/textarea.tsx","type":"registry:ui","target":"components/ui/textarea.tsx","content":"import type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/input-2.css\";\nimport \"./textarea.css\";\n\n/** A recessed multi-line field that grows with the draft from 3 to 8 lines, then scrolls. */\nexport function Textarea({ className, ...props }: ComponentProps<\"textarea\">) {\n  return (\n    <textarea\n      data-slot=\"textarea\"\n      className={cn(\n        \"block min-h-[88px] max-h-[200px] w-full min-w-0 resize-none rounded-button px-3 py-[9px] text-base leading-[22px] text-foreground outline-none [field-sizing:content] placeholder:text-muted-foreground disabled:cursor-not-allowed md:text-sm md:leading-[22px]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n"},{"path":"ui/shared/alert.css","type":"registry:file","target":"components/ui/shared/alert.css","content":"/* Shared by alert.tsx, toast.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"feedback-message\"] {\n    display: block;\n    animation: caliper-fade-in var(--duration-normal) var(--ease-out);\n  }\n  :is([data-slot=\"alert\"], [data-slot=\"toast\"])[data-variant=\"success\"]\n    [data-slot=\"feedback-icon\"] {\n    background: var(--success-subtle);\n    color: var(--success);\n  }\n  :is([data-slot=\"alert\"], [data-slot=\"toast\"])[data-variant=\"warning\"]\n    [data-slot=\"feedback-icon\"] {\n    background: var(--warning-subtle);\n    color: var(--warning);\n  }\n  :is([data-slot=\"alert\"], [data-slot=\"toast\"])[data-variant=\"error\"]\n    [data-slot=\"feedback-icon\"] {\n    background: var(--destructive-subtle);\n    color: var(--destructive);\n  }\n  @container (max-width: 22rem) {\n    [data-slot=\"feedback-copy\"] {\n      display: contents;\n    }\n    [data-slot=\"feedback-title\"] {\n      grid-column: 2;\n      grid-row: 1;\n      min-width: 0;\n      padding-top: 0.25rem;\n    }\n    [data-slot=\"feedback-description\"],\n    [data-slot=\"feedback-action\"] {\n      grid-column: 1 / -1;\n      margin-top: 0;\n    }\n    [data-slot=\"feedback-description\"] {\n      grid-row: 2;\n    }\n    [data-slot=\"feedback-action\"] {\n      grid-row: 3;\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"feedback-message\"] {\n      animation: none;\n    }\n  }\n}\n"},{"path":"ui/alert.css","type":"registry:file","target":"components/ui/alert.css","content":"/* alert.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"alert\"] {\n    container-type: inline-size;\n    opacity: 1;\n    transform: translateY(0);\n    transition-property: opacity, transform, display;\n    transition-duration: var(--duration-normal);\n    transition-timing-function: var(--ease-out);\n    transition-behavior: allow-discrete;\n  }\n  [data-slot=\"alert\"][hidden] {\n    display: none !important;\n    opacity: 0;\n    pointer-events: none;\n    transition-duration: var(--duration-fast);\n  }\n  @starting-style {\n    [data-slot=\"alert\"]:not([hidden]) {\n      opacity: 0;\n      transform: translateY(var(--motion-enter-distance));\n    }\n  }\n  @container (max-width: 22rem) {\n    [data-slot=\"alert\"] > [data-slot=\"button\"] {\n      grid-column: 3;\n      grid-row: 1;\n    }\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"alert\"] {\n      transition: none;\n      transform: none !important;\n    }\n    @starting-style {\n      [data-slot=\"alert\"]:not([hidden]) {\n        opacity: 1;\n        transform: none;\n      }\n    }\n  }\n}\n"},{"path":"ui/shared/loading-indicator.css","type":"registry:file","target":"components/ui/shared/loading-indicator.css","content":"/* Shared by button.tsx, confirm-morph.tsx, expanding-button-group.tsx, field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"loading-indicator\"] {\n    animation: caliper-spin var(--duration-loading) var(--ease-linear) infinite;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"loading-indicator\"] {\n      animation: none;\n    }\n  }\n}\n"},{"path":"ui/button.css","type":"registry:file","target":"components/ui/button.css","content":"/* button.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"button\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Button material. Raised to press: a lit top edge and a small lift; the\n     press travels 1px down and swaps the lift for an inner shade. Only\n     translate animates (release on the settle curve); shadows swap. */\n  [data-slot=\"button\"] {\n    --b-bg: var(--raised);\n    --b-bg-hover: var(--raised-hover);\n    --b-bg-press: var(--raised-press);\n    --b-fg: var(--foreground);\n    --b-shadow: var(--shadow-raised);\n    --b-shadow-press: var(--shadow-raised-press);\n    background-color: var(--b-bg);\n    color: var(--b-fg);\n    box-shadow: var(--b-shadow);\n    transition-property: background-color, color, border-color, translate, scale;\n    transition-duration:\n      var(--duration-fast), var(--duration-fast), var(--duration-fast),\n      var(--duration-normal), var(--duration-normal);\n    transition-timing-function:\n      var(--ease-out), var(--ease-out), var(--ease-out), var(--ease-settle),\n      var(--ease-settle);\n  }\n  [data-slot=\"button\"][data-variant=\"default\"] {\n    --b-bg: var(--primary);\n    --b-bg-hover: var(--primary-hover);\n    --b-bg-press: var(--primary-press);\n    --b-fg: var(--primary-foreground);\n    --b-shadow: var(--shadow-accent);\n    --b-shadow-press: var(--shadow-accent-press);\n  }\n  [data-slot=\"button\"][data-variant=\"destructive\"] {\n    --b-bg: var(--danger-fill);\n    --b-bg-hover: var(--danger-fill-hover);\n    --b-bg-press: var(--danger-fill-press);\n    --b-fg: var(--danger-fill-foreground);\n    --b-shadow: var(--shadow-danger);\n    --b-shadow-press: var(--shadow-danger-press);\n  }\n  [data-slot=\"button\"][data-variant=\"ghost\"] {\n    --b-bg: transparent;\n    --b-bg-hover: var(--muted);\n    --b-bg-press: var(--muted);\n    --b-fg: var(--muted-foreground);\n    --b-shadow: none;\n    --b-shadow-press: none;\n  }\n  @media (hover: hover) {\n    [data-slot=\"button\"]:hover {\n      background-color: var(--b-bg-hover);\n    }\n    [data-slot=\"button\"][data-variant=\"ghost\"]:hover {\n      color: var(--foreground);\n    }\n  }\n  [data-slot=\"button\"]:active:not(:disabled):not([aria-disabled=\"true\"]):not(\n      [aria-busy=\"true\"]\n    ) {\n    translate: 0 var(--motion-press-travel);\n    scale: var(--motion-press-scale);\n    background-color: var(--b-bg-press);\n    box-shadow: var(--b-shadow-press);\n    transition-duration:\n      var(--duration-fast), var(--duration-fast), var(--duration-fast), 90ms,\n      90ms;\n    transition-timing-function: var(--ease-out);\n  }\n  [data-slot=\"button\"]:is(:disabled, [aria-disabled=\"true\"]):not([aria-busy=\"true\"]) {\n    background-color: var(--sunken);\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"button\"][data-variant=\"ghost\"]:is(:disabled, [aria-disabled=\"true\"]):not(\n      [aria-busy=\"true\"]\n    ) {\n    background-color: transparent;\n    box-shadow: none;\n  }\n  /* Loading keeps the exact width: the label stays in flow but invisible. */\n  [data-slot=\"button-label\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: inherit;\n  }\n  /* Opacity, not visibility, so the label still names the button. */\n  [data-slot=\"button-label\"][data-hidden=\"true\"] {\n    opacity: 0;\n  }\n  [data-slot=\"button-busy\"] {\n    position: absolute;\n    inset: 0;\n    display: grid;\n    place-items: center;\n  }\n  [data-slot=\"button-feedback\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: inherit;\n    overflow: clip;\n    transition: width var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"button-feedback-label\"] {\n    display: inline-flex;\n    flex-shrink: 0;\n    align-items: center;\n    gap: inherit;\n    width: max-content;\n  }\n  [data-slot=\"button-feedback-label\"][data-enter=\"true\"] {\n    animation: caliper-feedback-enter var(--duration-normal) var(--ease-out);\n  }\n\n  [data-slot=\"button\"][aria-busy=\"true\"] {\n    pointer-events: none;\n  }\n  [data-slot=\"button\"][data-action-phase=\"error\"] {\n    color: var(--danger);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"button\"],\n    [data-slot=\"button-feedback\"] {\n      transition: none;\n    }\n    [data-slot=\"button\"]:active:not(:disabled):not([aria-disabled=\"true\"]):not(\n        [aria-busy=\"true\"]\n      ) {\n      translate: none;\n    }\n    [data-slot=\"button-feedback-label\"],\n    [data-slot=\"button-feedback-label\"][data-enter=\"true\"] {\n      animation: none;\n    }\n  }\n\n  /* Toggle: pressed is recessed, as a toggled tool in a button group is. */\n  /* Four attributes, so it wins over the group and toolbar pressed rules in any stylesheet order. */\n  [data-slot=\"button\"][data-variant=\"ghost\"][data-toggle][aria-pressed=\"true\"] {\n    --b-bg: var(--raised-press);\n    --b-bg-hover: var(--raised-press);\n    --b-bg-press: var(--raised-press);\n    --b-fg: var(--foreground);\n    --b-shadow: var(--shadow-sunken);\n    --b-shadow-press: var(--shadow-sunken);\n  }\n}\n"},{"path":"ui/feedback.css","type":"registry:file","target":"components/ui/feedback.css","content":"/* feedback.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"feedback-icon\"] {\n    display: flex;\n    width: 2rem;\n    height: 2rem;\n    flex-shrink: 0;\n    align-items: center;\n    justify-content: center;\n    border-radius: var(--radius-surface);\n    background: var(--muted);\n    color: var(--muted-foreground);\n    transition:\n      color var(--duration-fast) var(--ease-out),\n      background-color var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"feedback-symbol\"] {\n    display: flex;\n    animation: caliper-feedback-symbol-enter var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"feedback-icon\"] svg {\n    width: 1rem;\n    height: 1rem;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"feedback-icon\"] {\n      transition: none;\n      transform: none !important;\n    }\n    [data-slot=\"feedback-symbol\"] {\n      animation: none;\n    }\n  }\n}\n"},{"path":"ui/shared/field-message.css","type":"registry:file","target":"components/ui/shared/field-message.css","content":"/* Shared by date-input.tsx, field.tsx, scrub-field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"field-message\"] {\n    display: flex;\n    align-items: flex-start;\n    gap: 6px;\n    min-height: 18px;\n    margin: 0;\n    font-size: 12px;\n    line-height: 18px;\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"field-message\"] svg {\n    flex: none;\n    width: 14px;\n    height: 14px;\n    margin-top: 2px;\n  }\n  [data-slot=\"field-message\"][data-tone=\"error\"] {\n    color: var(--danger);\n  }\n  [data-slot=\"field-message\"][data-tone=\"success\"] {\n    color: var(--success);\n  }\n}\n"},{"path":"ui/field.css","type":"registry:file","target":"components/ui/field.css","content":"/* field.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"field-count\"] {\n    margin-left: auto;\n    color: var(--muted-foreground);\n    font-variant-numeric: tabular-nums;\n    font-size: 12px;\n    font-variant-numeric: tabular-nums;\n  }\n}\n"},{"path":"ui/shared/input.css","type":"registry:file","target":"components/ui/shared/input.css","content":"/* Shared by combobox.tsx, input.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"input\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"input\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"input\"] {\n      transition: none;\n    }\n  }\n}\n"},{"path":"ui/shared/input-2.css","type":"registry:file","target":"components/ui/shared/input-2.css","content":"/* Shared by combobox.tsx, input.tsx, select-menu.tsx, select.tsx, textarea.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  @media (hover: hover) {\n    :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"]):hover:enabled:not(:focus-visible):not([aria-invalid=\"true\"]) {\n      box-shadow: var(--shadow-field-hover);\n    }\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"]):focus-visible {\n    outline: none;\n    box-shadow: var(--shadow-field-focus);\n    transition: none;\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"])[aria-invalid=\"true\"] {\n    box-shadow: var(--shadow-field-invalid);\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"])[aria-invalid=\"true\"]:focus-visible {\n    box-shadow:\n      var(--shadow-field-invalid),\n      0 0 0 4px color-mix(in srgb, var(--danger) 18%, transparent);\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"], [data-slot=\"select\"]):disabled {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n    color: var(--muted-foreground);\n  }\n  :is([data-slot=\"input\"], [data-slot=\"textarea\"])[readonly] {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n  }\n}\n"},{"path":"ui/shared/input-group.css","type":"registry:file","target":"components/ui/shared/input-group.css","content":"/* Shared by input.tsx, money-field.tsx, phone-field.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  @media (hover: hover) {\n    :is([data-slot=\"input-group\"], [data-slot=\"money-field\"], [data-slot=\"phone-field-control\"]):not([data-disabled]):not([data-invalid]):hover:not(:focus-within) {\n      box-shadow: var(--shadow-field-hover);\n    }\n  }\n  :is([data-slot=\"input-group\"], [data-slot=\"money-field\"], [data-slot=\"phone-field-control\"]):focus-within {\n    outline: none;\n    box-shadow: var(--shadow-field-focus);\n    transition: none;\n  }\n}\n"},{"path":"ui/input.css","type":"registry:file","target":"components/ui/input.css","content":"/* input.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"input-group\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"input-group\"][data-invalid] {\n    box-shadow: var(--shadow-field-invalid);\n  }\n  [data-slot=\"input-group\"][data-invalid]:focus-within {\n    box-shadow:\n      var(--shadow-field-invalid),\n      0 0 0 4px color-mix(in srgb, var(--danger) 18%, transparent);\n  }\n  [data-slot=\"input-group\"][data-disabled] {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"input-group\"][data-readonly] {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n  }\n  [data-slot=\"input-bare\"] {\n    border: 0;\n    box-shadow: none;\n    outline: none;\n  }\n  [data-slot=\"input-affix\"] {\n    display: inline-flex;\n    align-items: center;\n    height: 20px;\n    padding-inline-end: 10px;\n    margin-inline-end: 2px;\n    color: var(--muted-foreground);\n    box-shadow: 1px 0 0 var(--border);\n    white-space: nowrap;\n  }\n  [data-slot=\"input-affix\"]:dir(rtl) {\n    box-shadow: -1px 0 0 var(--border);\n  }\n  [data-slot=\"input-affix\"]:has(> svg:only-child) {\n    padding-inline-end: 0;\n    box-shadow: none;\n  }\n  [data-slot=\"input-trailing\"] {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    color: var(--muted-foreground);\n    white-space: nowrap;\n    font-size: 12px;\n  }\n  :is([data-slot=\"input-affix\"], [data-slot=\"input-trailing\"]) svg {\n    width: 16px;\n    height: 16px;\n  }\n  [data-slot=\"docs-toolbar\"] [data-slot=\"input-group\"] { max-width: 24rem; }\n}\n"},{"path":"ui/shared/select.css","type":"registry:file","target":"components/ui/shared/select.css","content":"/* Shared by select-menu.tsx, select.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"select\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  [data-slot=\"select\"] {\n    cursor: pointer;\n    touch-action: manipulation;\n  }\n  /* The native select wears the select-menu trigger: no platform arrow, the\n     same up/down mark on the right. Lists (multiple or size > 1) keep theirs. */\n  [data-slot=\"select\"]:not([multiple]):not([size]) {\n    appearance: none;\n    padding-right: 36px;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%235d5d66' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 15 5 5 5-5'/%3E%3Cpath d='m7 9 5-5 5 5'/%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    background-position: right 10px center;\n    background-size: 16px 16px;\n  }\n  [data-theme=\"dark\"] [data-slot=\"select\"]:not([multiple]):not([size]),\n  [data-slot=\"select\"][data-theme=\"dark\"]:not([multiple]):not([size]) {\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23ababb4' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 15 5 5 5-5'/%3E%3Cpath d='m7 9 5-5 5 5'/%3E%3C/svg%3E\");\n  }\n\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"select\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n\n  [data-slot=\"select\"]:disabled {\n    cursor: not-allowed;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"select\"] {\n      transition: none;\n    }\n  }\n}\n"},{"path":"ui/select-menu.css","type":"registry:file","target":"components/ui/select-menu.css","content":"/* select-menu.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"select-menu-native\"] [data-slot=\"select\"] {\n    background-image: none;\n  }\n  /* Select menu: the field trigger, and a frosted list that opens with the\n     chosen row over the trigger and grows around it. */\n  [data-slot=\"select-menu\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    cursor: pointer;\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n  @media (hover: hover) {\n    [data-slot=\"select-menu\"]:hover:enabled:not(:focus-visible):not([aria-expanded=\"true\"]):not([aria-invalid=\"true\"]) {\n      box-shadow: var(--shadow-field-hover);\n    }\n  }\n  [data-slot=\"select-menu\"]:focus-visible,\n  [data-slot=\"select-menu\"][aria-expanded=\"true\"] {\n    box-shadow: var(--shadow-field-focus);\n  }\n  [data-slot=\"select-menu\"][aria-invalid=\"true\"] {\n    box-shadow: var(--shadow-field-invalid);\n  }\n  [data-slot=\"select-menu\"]:disabled {\n    background-color: var(--sunken);\n    box-shadow: var(--shadow-field-quiet);\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"select-menu-code\"] {\n    margin-left: auto;\n    font-family: var(--font-mono);\n    font-size: 12px;\n    color: var(--muted-foreground);\n  }\n  [data-slot=\"select-menu-content\"] {\n    z-index: 50;\n    border-radius: var(--radius-surface);\n    background: var(--elevated-glass);\n    -webkit-backdrop-filter: blur(var(--blur-overlay));\n    backdrop-filter: blur(var(--blur-overlay));\n    box-shadow: var(--shadow-overlay), 0 0 0 1px var(--edge-glow), inset 0 1px 0 var(--edge-light);\n    color: var(--foreground);\n    animation: caliper-select-in var(--duration-normal) var(--ease-out);\n  }\n  [data-slot=\"select-menu-option\"] {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    height: 36px;\n    padding: 0 10px;\n    border-radius: var(--radius-control);\n    font-size: 14px;\n    cursor: default;\n    transition: background-color var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"select-menu-option\"][data-active=\"true\"] {\n    background: var(--selected);\n  }\n  [data-slot=\"select-menu-option\"][aria-disabled=\"true\"] {\n    color: var(--muted-foreground);\n    opacity: 0.7;\n  }\n  [data-slot=\"select-menu-native\"] [data-slot=\"select\"] {\n    appearance: none;\n  }\n  [data-slot=\"select-menu-icon\"] {\n    display: inline-grid;\n    flex: none;\n    place-items: center;\n    width: 16px;\n    height: 16px;\n  }\n  [data-slot=\"select-menu-icon\"] svg { width: 16px; height: 16px; }\n  /* Ghost: no field ring, for a select inside a composed control. */\n  [data-slot=\"select-menu\"][data-variant=\"ghost\"],\n  [data-slot=\"select-menu-native\"][data-variant=\"ghost\"] [data-slot=\"select\"] {\n    background-color: transparent;\n    box-shadow: none;\n  }\n  @media (hover: hover) {\n    [data-slot=\"select-menu\"][data-variant=\"ghost\"]:hover:enabled:not([aria-expanded=\"true\"]) {\n      background-color: var(--raised-hover);\n      box-shadow: none;\n    }\n  }\n  [data-slot=\"select-menu\"][data-variant=\"ghost\"]:focus-visible,\n  [data-slot=\"select-menu\"][data-variant=\"ghost\"][aria-expanded=\"true\"] {\n    background-color: var(--raised-hover);\n    box-shadow: 0 0 0 2px var(--ring);\n  }\n  [data-slot=\"select-menu\"][data-variant=\"ghost\"]:disabled {\n    background-color: transparent;\n    box-shadow: none;\n  }\n  [data-slot=\"select-menu\"][data-size=\"sm\"] [data-slot=\"select-menu-code\"] { font-size: 12px; }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"select-menu-content\"] {\n      animation: none;\n    }\n  }\n  [data-slot=\"toolbar\"] [data-toolbar-generic] [data-slot=\"select-menu\"] { height: calc(var(--control-height) - 6px); }\n}\n"},{"path":"ui/textarea.css","type":"registry:file","target":"components/ui/textarea.css","content":"/* textarea.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"textarea\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n\n  /* Recessed to fill. One ring for every field: 3:1 at rest, darker on\n     hover, a 2px ember ring with a soft halo on focus, danger when invalid.\n     Disabled and read-only drop into the sunken fill and stay legible. */\n  [data-slot=\"textarea\"] {\n    border: 0;\n    background-color: var(--field);\n    box-shadow: var(--shadow-field);\n    transition: box-shadow var(--duration-fast) var(--ease-out);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"textarea\"] {\n      transition: none;\n    }\n  }\n}\n"}],"categories":["free-marketing"],"meta":{"tier":"free","license":"MIT","family":"marketing","docs":"/docs/components/contact-section"}}