{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"command-palette","type":"registry:ui","title":"Command palette","description":"A searchable list of destinations and actions opened from the keyboard.","dependencies":["@radix-ui/react-dialog","@radix-ui/react-slot","clsx","lucide-react","tailwind-merge"],"registryDependencies":["https://caliperui.site/r/styles.json"],"files":[{"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/command-palette.tsx","type":"registry:ui","target":"components/ui/command-palette.tsx","content":"\"use client\";\n\nimport { Search, X } from \"lucide-react\";\nimport {\n  useEffect,\n  useLayoutEffect,\n  useId,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { Button } from \"./button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogTitle,\n  type DialogContentProps,\n} from \"./dialog\";\nimport { Input } from \"./input\";\nimport { cn } from \"./lib/cn\";\nimport \"./command-palette.css\";\n\nexport type CommandItem = {\n  id: string;\n  label: string;\n  description?: string;\n  group?: string;\n  icon?: ReactNode;\n  keywords?: readonly string[];\n  disabled?: boolean;\n  /** One disambiguating fact on the right, in mono: an amount, a role, a shortcut. */\n  meta?: string;\n  /** Shown in the side pane while the item is highlighted. */\n  preview?: CommandPreview;\n  onSelect: () => void;\n};\n\nexport type CommandPreview = {\n  kind: string;\n  title: string;\n  rows?: readonly (readonly [string, string])[];\n  hint?: string;\n};\n\n/** Typing the prefix narrows the list to one group, shown as a chip. */\nexport type CommandScope = { prefix: string; label: string; group: string };\n\nexport type CommandPaletteProps = {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  items: readonly CommandItem[];\n  title?: string;\n  description?: string;\n  className?: string;\n  onCloseAutoFocus?: DialogContentProps[\"onCloseAutoFocus\"];\n  scopes?: readonly CommandScope[];\n  /** Offered when nothing matches, e.g. create an invoice for the typed name. */\n  emptyAction?: { label: (query: string) => string; onSelect: (query: string) => void };\n  /** The search field's prompt, naming what can be found, e.g. \"Search people, companies and deals…\". */\n  placeholder?: string;\n  /** What one item is called in the count and the empty state, singular and plural: [\"result\", \"results\"] for a site search. */\n  noun?: readonly [string, string];\n};\n\nexport function CommandPalette({\n  open,\n  onOpenChange,\n  items,\n  title = \"Command palette\",\n  description = \"Search destinations, projects, and actions.\",\n  className,\n  onCloseAutoFocus,\n  scopes = [],\n  emptyAction,\n  placeholder = \"Find a project or action…\",\n  noun = [\"command\", \"commands\"],\n}: CommandPaletteProps) {\n  const hasPreview = items.some((item) => item.preview);\n  // Esc leaves an active scope first; without a scope it closes.\n  const clearOnEscape = useRef<(() => boolean) | null>(null);\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent\n        className={cn(\n          // Phones: pinned to the top, because the keyboard covers a sheet on the bottom edge and iOS won't move it.\n          \"flex flex-col gap-0 overflow-hidden rounded-overlay p-0 sm:p-0 max-sm:top-2 max-sm:bottom-auto\",\n          hasPreview ? \"max-w-2xl\" : \"max-w-xl\",\n          className,\n        )}\n        onCloseAutoFocus={onCloseAutoFocus}\n        onEscapeKeyDown={(event) => {\n          if (clearOnEscape.current?.()) event.preventDefault();\n        }}\n      >\n        <PaletteContent\n          items={items}\n          title={title}\n          description={description}\n          interactive={open}\n          scopes={scopes}\n          emptyAction={emptyAction}\n          placeholder={placeholder}\n          noun={noun}\n          hasPreview={hasPreview}\n          clearOnEscape={clearOnEscape}\n          onSelect={(item) => {\n            onOpenChange(false);\n            item.onSelect();\n          }}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nfunction PaletteContent({\n  items,\n  title,\n  description,\n  interactive,\n  onSelect,\n  scopes,\n  emptyAction,\n  placeholder,\n  noun,\n  hasPreview,\n  clearOnEscape,\n}: {\n  items: readonly CommandItem[];\n  title: string;\n  description: string;\n  interactive: boolean;\n  onSelect: (item: CommandItem) => void;\n  scopes: readonly CommandScope[];\n  emptyAction?: CommandPaletteProps[\"emptyAction\"];\n  placeholder: string;\n  noun: readonly [string, string];\n  hasPreview: boolean;\n  clearOnEscape: { current: (() => boolean) | null };\n}) {\n  const id = useId();\n  const input = useRef<HTMLInputElement>(null);\n  const activeOption = useRef<HTMLDivElement>(null);\n  const options = useRef<HTMLDivElement>(null);\n  const [highlight, setHighlight] = useState<{\n    top: number;\n    height: number;\n  } | null>(null);\n  const [query, setQuery] = useState(\"\");\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const [scopeId, setScopeId] = useState<string | null>(null);\n  const scope = scopes.find((candidate) => candidate.prefix === scopeId);\n  const search = query.trim().toLowerCase();\n  const showIcons = items.some((item) => Boolean(item.icon));\n  const groups = new Map<string, CommandItem[]>();\n  for (const item of items) {\n    const group = item.group ?? \"\";\n    if (scope && group !== scope.group) continue;\n    const grouped = groups.get(group) ?? [];\n    groups.set(group, grouped);\n    const text = [item.label, item.description, ...(item.keywords ?? [])]\n      .join(\" \")\n      .toLowerCase();\n    if (text.includes(search)) grouped.push(item);\n  }\n  const visibleGroups = [...groups.entries()].filter(\n    ([, commands]) => commands.length,\n  );\n  const matched = visibleGroups.flatMap(([, commands]) => commands);\n  const enabled = matched.filter((item) => !item.disabled);\n  const active = enabled.find((item) => item.id === activeId) ?? enabled[0];\n  const optionIds = new Map(\n    matched.map((item, index) => [item.id, `${id}-option-${index}`]),\n  );\n  const listId = `${id}-commands`;\n\n  useEffect(() => {\n    activeOption.current?.scrollIntoView({\n      block: \"nearest\",\n      behavior: \"instant\",\n    });\n  }, [active?.id, search]);\n\n  useLayoutEffect(() => {\n    const list = options.current;\n    const option = activeOption.current;\n    if (!list || !option) {\n      setHighlight(null);\n      return;\n    }\n    const measure = () => {\n      setHighlight({\n        top: option.offsetTop,\n        height: option.offsetHeight,\n      });\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(list);\n    observer.observe(option);\n    return () => observer.disconnect();\n  }, [active?.id, search]);\n\n  function select(item: CommandItem) {\n    if (interactive && !item.disabled) onSelect(item);\n  }\n\n  useEffect(() => {\n    clearOnEscape.current = () => {\n      if (!scope) return false;\n      setScopeId(null);\n      setActiveId(null);\n      return true;\n    };\n  });\n\n  function mark(text: string) {\n    const at = search ? text.toLowerCase().indexOf(search) : -1;\n    if (at < 0) return text;\n    return (\n      <>\n        {text.slice(0, at)}\n        <mark data-slot=\"command-match\">{text.slice(at, at + search.length)}</mark>\n        {text.slice(at + search.length)}\n      </>\n    );\n  }\n\n  function clearSearch() {\n    setQuery(\"\");\n    setScopeId(null);\n    setActiveId(null);\n    input.current?.focus();\n  }\n\n  return (\n    <>\n      <div className=\"shrink-0 border-b border-border p-4\">\n        <DialogTitle className=\"pr-12 text-sm leading-5\">{title}</DialogTitle>\n        <DialogDescription className=\"sr-only\">{description}</DialogDescription>\n        <label htmlFor={`${id}-search`} className=\"sr-only\">\n          Search {noun[1]}\n        </label>\n        <div className=\"relative mt-3 flex items-center gap-2 rounded-surface bg-surface pl-3 focus-within:outline-solid focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring\">\n          <Search\n            className=\"pointer-events-none size-4 shrink-0 text-muted-foreground\"\n            aria-hidden=\"true\"\n          />\n          {scope && <span data-slot=\"command-scope\">{scope.label}</span>}\n          <Input\n            ref={input}\n            id={`${id}-search`}\n            role=\"combobox\"\n            aria-autocomplete=\"list\"\n            aria-expanded={interactive}\n            aria-controls={listId}\n            aria-activedescendant={\n              active ? optionIds.get(active.id) : undefined\n            }\n            autoComplete=\"off\"\n            spellCheck={false}\n            placeholder={placeholder}\n            value={query}\n            readOnly={!interactive}\n            className=\"flex-1 border-0 bg-transparent pl-0 pr-11 shadow-none focus-visible:shadow-none\"\n            onChange={(event) => {\n              if (!interactive) return;\n              const value = event.target.value;\n              // Typing a scope prefix first turns it into a chip.\n              const typed = !scope && scopes.find((candidate) => value.startsWith(candidate.prefix));\n              if (typed) {\n                setScopeId(typed.prefix);\n                setQuery(value.slice(typed.prefix.length));\n              } else {\n                setQuery(value);\n              }\n              setActiveId(null);\n            }}\n            onKeyDown={(event) => {\n              if (event.key === \"Backspace\" && !query && scope) {\n                event.preventDefault();\n                setScopeId(null);\n                return;\n              }\n              if (\n                !interactive ||\n                event.nativeEvent.isComposing ||\n                event.altKey ||\n                event.ctrlKey ||\n                event.metaKey ||\n                event.shiftKey\n              )\n                return;\n              if (event.key === \"Enter\" && active) {\n                event.preventDefault();\n                select(active);\n              } else if (\n                enabled.length &&\n                ([\"ArrowDown\", \"ArrowUp\"].includes(event.key) ||\n                  (!query && [\"Home\", \"End\"].includes(event.key)))\n              ) {\n                event.preventDefault();\n                const current = enabled.findIndex(\n                  (item) => item.id === active?.id,\n                );\n                const next =\n                  event.key === \"Home\"\n                    ? 0\n                    : event.key === \"End\"\n                      ? enabled.length - 1\n                      : (current +\n                          (event.key === \"ArrowDown\" ? 1 : -1) +\n                          enabled.length) %\n                        enabled.length;\n                setActiveId(enabled[next].id);\n              }\n            }}\n          />\n          {query && (\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"absolute top-0 right-0\"\n              aria-label=\"Clear command search\"\n              onClick={clearSearch}\n            >\n              <X className=\"size-4\" aria-hidden=\"true\" />\n            </Button>\n          )}\n        </div>\n      </div>\n\n      <div className={cn(\"grid min-h-0\", hasPreview && \"md:grid-cols-[minmax(0,1fr)_240px]\")}>\n      <div className=\"min-h-0 max-h-[min(28rem,60dvh)] overflow-y-auto overscroll-contain p-2 max-sm:max-h-[33dvh]\">\n        <div\n          ref={options}\n          id={listId}\n          role=\"listbox\"\n          aria-label=\"Commands\"\n          className=\"relative isolate\"\n        >\n          {highlight && (\n            <div\n              key={search}\n              data-slot=\"command-highlight\"\n              aria-hidden=\"true\"\n              style={{\n                height: highlight.height,\n                transform: `translateY(${highlight.top}px)`,\n              }}\n            />\n          )}\n          {visibleGroups.map(([group, commands], groupIndex) => (\n            <div key={group} role=\"group\" aria-label={group || undefined}>\n              {group && (\n                <p\n                  className={cn(\n                    \"px-3 pb-1 text-xs font-medium text-muted-foreground\",\n                    groupIndex ? \"pt-3\" : \"pt-1\",\n                  )}\n                >\n                  {group}\n                </p>\n              )}\n              {commands.map((item) => {\n                const selected = active?.id === item.id;\n                return (\n                  <div\n                    data-slot=\"command-option\"\n                    key={item.id}\n                    id={optionIds.get(item.id)}\n                    ref={selected ? activeOption : undefined}\n                    role=\"option\"\n                    aria-selected={selected}\n                    aria-disabled={item.disabled || undefined}\n                    className={cn(\n                      \"flex min-h-[var(--control-height)] cursor-pointer items-center gap-3 rounded-surface px-3 text-sm font-medium\",\n                      item.description && \"py-2\",\n                      \"text-foreground\",\n                      item.disabled &&\n                        \"cursor-not-allowed text-muted-foreground hover:bg-transparent\",\n                    )}\n                    onMouseDown={(event) => event.preventDefault()}\n                    onMouseEnter={() => {\n                      if (!item.disabled && interactive) setActiveId(item.id);\n                    }}\n                    onClick={() => select(item)}\n                  >\n                    {showIcons && (\n                      <span\n                        className=\"flex size-5 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4\"\n                        aria-hidden=\"true\"\n                      >\n                        {item.icon}\n                      </span>\n                    )}\n                    <span className=\"min-w-0 flex-1\">\n                      <span className=\"block text-sm leading-5 font-medium break-words [overflow-wrap:anywhere]\">\n                        {mark(item.label)}\n                      </span>\n                      {item.description && (\n                        <span className=\"block text-xs leading-4 text-muted-foreground break-words [overflow-wrap:anywhere]\">\n                          {item.description}\n                        </span>\n                      )}\n                    </span>\n                    {item.meta && !item.disabled && (\n                      <span className=\"shrink-0 tabular-nums text-xs text-muted-foreground\">\n                        {item.meta}\n                      </span>\n                    )}\n                    {item.disabled && (\n                      <span className=\"shrink-0 text-xs text-muted-foreground\">\n                        Unavailable\n                      </span>\n                    )}\n                  </div>\n                );\n              })}\n            </div>\n          ))}\n        </div>\n        {!matched.length && (\n          <div className=\"px-3 py-6 text-center\">\n            <p className=\"text-sm font-medium\">No {noun[1]} found</p>\n            <p className=\"mt-1 text-xs leading-5 text-muted-foreground break-words [overflow-wrap:anywhere]\">\n              {search\n                ? `Nothing matches “${query.trim()}”. Try another word or a shorter one.`\n                : `There are no ${noun[1]} in this context.`}\n            </p>\n            {search && (\n              <div className=\"mt-4 flex flex-wrap justify-center gap-2\">\n                {emptyAction && (\n                  <Button\n                    variant=\"default\"\n                    size=\"sm\"\n                    onClick={() => emptyAction.onSelect(search)}\n                  >\n                    {emptyAction.label(search)}\n                  </Button>\n                )}\n                <Button variant=\"secondary\" size=\"sm\" onClick={clearSearch}>\n                  Clear search\n                </Button>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n      {hasPreview && (\n        <div data-slot=\"command-preview\" aria-live=\"polite\" className=\"hidden md:flex\">\n          {active?.preview ? (\n            <>\n              <span className=\"tabular-nums text-[11px] tracking-[0.06em] text-muted-foreground uppercase\">{active.preview.kind}</span>\n              <span className=\"text-base leading-6 font-semibold\">{active.preview.title}</span>\n              {active.preview.rows && (\n                <dl className=\"grid grid-cols-[72px_minmax(0,1fr)] gap-x-2.5 gap-y-1.5 text-[13px] leading-[18px]\">\n                  {active.preview.rows.map(([key, value]) => (\n                    <div key={key} className=\"contents\">\n                      <dt className=\"text-muted-foreground\">{key}</dt>\n                      <dd className=\"m-0 tabular-nums\">{value}</dd>\n                    </div>\n                  ))}\n                </dl>\n              )}\n              {active.preview.hint && <span className=\"mt-auto text-xs text-muted-foreground\">{active.preview.hint}</span>}\n            </>\n          ) : (\n            <span className=\"text-xs text-muted-foreground\">Highlight a result to preview it.</span>\n          )}\n        </div>\n      )}\n      </div>\n\n      <div className=\"flex shrink-0 flex-wrap items-center justify-between gap-x-4 gap-y-1 border-t border-border px-4 py-3 text-xs leading-5 text-muted-foreground\">\n        <p role=\"status\" aria-atomic=\"true\">\n          {matched.length} {matched.length === 1 ? noun[0] : noun[1]}\n        </p>\n        <p className=\"hidden sm:block\">\n          <span aria-hidden=\"true\">↑↓</span> Navigate · Enter Select · Esc Close\n        </p>\n      </div>\n    </>\n  );\n}\n"},{"path":"ui/dialog.tsx","type":"registry:ui","target":"components/ui/dialog.tsx","content":"\"use client\";\n\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport { Button } from \"./button\";\nimport \"./shared/dialog-overlay.css\";\nimport \"./dialog.css\";\n\nexport const Dialog = DialogPrimitive.Root;\nexport const DialogTrigger = DialogPrimitive.Trigger;\nexport const DialogClose = DialogPrimitive.Close;\n\nexport type DialogContentProps = ComponentProps<\n  typeof DialogPrimitive.Content\n> & {\n  showCloseButton?: boolean;\n};\n\nexport function DialogContent({\n  className,\n  children,\n  showCloseButton = true,\n  ...props\n}: DialogContentProps) {\n  return (\n    <DialogPrimitive.Portal>\n      <DialogPrimitive.Overlay\n        data-slot=\"dialog-overlay\"\n        className=\"fixed inset-0 z-50 bg-[var(--overlay)]\"\n      />\n      <DialogPrimitive.Content\n        data-slot=\"dialog-content\"\n        className={cn(\n          \"fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-5 overflow-y-auto rounded-overlay border border-border bg-elevated p-5 text-foreground shadow-overlay outline-none sm:p-6\",\n          // Phones: the dialog becomes a sheet resting on the bottom edge.\n          props[\"data-motion\" as keyof typeof props] !== \"sheet\" &&\n            \"max-sm:top-auto max-sm:bottom-2 max-sm:left-2 max-sm:w-[calc(100%-1rem)] max-sm:max-w-none max-sm:translate-x-0 max-sm:translate-y-0 max-sm:max-h-[calc(100dvh-1.5rem)]\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {showCloseButton && (\n          <DialogPrimitive.Close asChild>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"absolute top-3.5 right-3.5 size-8 rounded-control max-sm:size-11\"\n              aria-label=\"Close dialog\"\n            >\n              <X aria-hidden=\"true\" />\n            </Button>\n          </DialogPrimitive.Close>\n        )}\n      </DialogPrimitive.Content>\n    </DialogPrimitive.Portal>\n  );\n}\n\nexport function DialogHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"flex min-w-0 flex-col gap-2 pr-8\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DialogFooter({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\n        \"sticky bottom-[-1.25rem] z-10 -mx-5 -mb-5 flex flex-col-reverse gap-2 border-t border-border bg-[var(--elevated-glass)] px-4 py-3.5 sm:bottom-[-1.5rem] sm:-mx-6 sm:-mb-6 sm:flex-row sm:justify-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DialogTitle({\n  className,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Title>) {\n  return (\n    <DialogPrimitive.Title\n      className={cn(\n        \"text-base leading-6 font-semibold tracking-tight break-words\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function DialogDescription({\n  className,\n  ...props\n}: ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      className={cn(\"text-sm leading-5 text-muted-foreground\", className)}\n      {...props}\n    />\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/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/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/command-palette.css","type":"registry:file","target":"components/ui/command-palette.css","content":"/* command-palette.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"command-option\"] {\n    transition-property: background-color, color, border-color;\n    transition-duration: var(--duration-fast);\n    transition-timing-function: var(--ease-out);\n  }\n  [data-slot=\"command-highlight\"] {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    z-index: -1;\n    pointer-events: none;\n    border-radius: var(--radius-surface);\n    background: var(--muted);\n    transition:\n      transform var(--duration-normal) var(--ease-settle),\n      height var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"command-scope\"] {\n    display: inline-flex;\n    flex-shrink: 0;\n    align-items: center;\n    height: 22px;\n    padding: 0 8px;\n    border-radius: 8px;\n    font-size: 12px;\n    font-weight: 500;\n    color: var(--accent-text);\n    background: var(--brand-subtle);\n  }\n  [data-slot=\"command-match\"] {\n    background: none;\n    color: inherit;\n    font-weight: 600;\n    text-decoration: underline;\n    text-decoration-color: var(--ring);\n    text-decoration-thickness: 1.5px;\n    text-underline-offset: 3px;\n  }\n  [data-slot=\"command-preview\"] {\n    flex-direction: column;\n    gap: 10px;\n    padding: 16px;\n    box-shadow: inset 1px 0 0 var(--border);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"command-highlight\"],\n    [data-slot=\"command-option\"] {\n      transition: none;\n    }\n  }\n}\n"},{"path":"ui/shared/dialog-overlay.css","type":"registry:file","target":"components/ui/shared/dialog-overlay.css","content":"/* Shared by bottom-sheet.tsx, dialog.tsx, morphing-modal.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"dialog-overlay\"][data-state=\"open\"] {\n    animation: caliper-fade-in var(--duration-normal) var(--ease-out);\n  }\n\n  [data-slot=\"dialog-overlay\"][data-state=\"closed\"] {\n    animation: caliper-fade-out var(--duration-fast) var(--ease-in) forwards;\n    pointer-events: none;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"dialog-overlay\"] {\n      animation: none;\n    }\n    /* Preserve the exit name for Radix Presence's completion matcher, including\n       a preference changed during exit. No visual motion or unmount timer. */\n    [data-slot=\"dialog-overlay\"][data-state=\"closed\"] {\n      animation: caliper-fade-out var(--duration-instant) var(--ease-in) forwards !important;\n    }\n  }\n}\n\n/* Outside the component layer so this wins over bg-elevated utilities. */\n[data-slot=\"dialog-overlay\"] {\n  backdrop-filter: blur(2px);\n}\n"},{"path":"ui/dialog.css","type":"registry:file","target":"components/ui/dialog.css","content":"/* dialog.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"dialog-content\"][data-motion=\"sheet\"][data-state=\"open\"] {\n    animation: caliper-fade-in var(--duration-normal) var(--ease-out);\n  }\n\n  [data-slot=\"dialog-content\"]:not([data-motion=\"sheet\"])[data-state=\"open\"] {\n    animation: caliper-dialog-enter var(--duration-normal) var(--ease-settle);\n  }\n\n  [data-slot=\"dialog-content\"][data-state=\"closed\"] {\n    animation: caliper-fade-out var(--duration-fast) var(--ease-in) forwards;\n    pointer-events: none;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"dialog-content\"],\n    [data-slot=\"dialog-content\"]:not([data-motion=\"sheet\"])[data-state=\"open\"] {\n      animation: none;\n    }\n    /* Preserve the exit name for Radix Presence's completion matcher, including\n       a preference changed during exit. No visual motion or unmount timer. */\n    [data-slot=\"dialog-content\"][data-state=\"closed\"] {\n      animation: caliper-fade-out var(--duration-instant) var(--ease-in) forwards !important;\n    }\n  }\n}\n\n[data-slot=\"dialog-content\"] {\n  background-color: var(--elevated-glass);\n  border-color: color-mix(in srgb, var(--edge-light) 45%, var(--border));\n  backdrop-filter: blur(var(--blur-overlay));\n  box-shadow:\n    var(--shadow-overlay),\n    0 0 40px var(--edge-glow),\n    inset 0 1px 0 var(--edge-light);\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"}],"categories":["overlays"],"meta":{"tier":"free","license":"MIT","family":"command-palette","docs":"/docs/components/command-palette"}}