{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"toolbar","type":"registry:ui","title":"Toolbar","description":"Tools with one tab stop: arrows move between them, toggles sit recessed, and when the row runs out of width the last tools fold into a More menu with their state.","dependencies":["@radix-ui/react-dropdown-menu","@radix-ui/react-slot","@radix-ui/react-tooltip","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/dropdown-menu.tsx","type":"registry:ui","target":"components/ui/dropdown-menu.tsx","content":"\"use client\";\n\nimport * as Primitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, Circle } from \"lucide-react\";\nimport { useEffect, useRef, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { GlideHighlight, useHighlightedGlide } from \"./glide-highlight\";\nimport { cn } from \"./lib/cn\";\nimport \"./shared/dropdown-content.css\";\nimport \"./dropdown-menu.css\";\n\nexport const DropdownMenu = Primitive.Root;\nexport const DropdownMenuTrigger = Primitive.Trigger;\nexport const DropdownMenuGroup = Primitive.Group;\nexport const DropdownMenuRadioGroup = Primitive.RadioGroup;\n\nexport function DropdownMenuContent({\n  className,\n  sideOffset = 8,\n  collisionPadding = 16,\n  children,\n  ...props\n}: ComponentProps<typeof Primitive.Content>) {\n  const [content, setContent] = useState<HTMLDivElement | null>(null);\n  const box = useHighlightedGlide(content);\n  return (\n    <Primitive.Portal>\n      <Primitive.Content\n        ref={setContent}\n        data-slot=\"dropdown-content\"\n        sideOffset={sideOffset}\n        collisionPadding={collisionPadding}\n        className={cn(\n          \"relative z-50 min-w-56 max-w-[calc(100vw-2rem)] max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto rounded-surface border border-border bg-elevated p-1 text-[13px] text-foreground shadow-overlay outline-none\",\n          className,\n        )}\n        {...props}\n      >\n        <GlideHighlight box={box} />\n        {children}\n      </Primitive.Content>\n    </Primitive.Portal>\n  );\n}\n\n/** Row anatomy shared by every menu: dropdown, menubar and context menus. */\nexport const menuItemClass =\n  \"relative z-10 flex min-h-[34px] max-md:min-h-[var(--control-height)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-[13px] font-normal outline-none data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[variant=destructive]:text-destructive [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground data-[variant=destructive]:[&_svg]:text-destructive\";\n\nexport function DropdownMenuItem({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Item>) {\n  return (\n    <Primitive.Item\n      data-slot=\"dropdown-item\"\n      className={cn(menuItemClass, className)}\n      {...props}\n    />\n  );\n}\nexport function DropdownMenuCheckboxItem({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof Primitive.CheckboxItem>) {\n  return (\n    <Primitive.CheckboxItem\n      data-slot=\"dropdown-item\"\n      className={cn(menuItemClass, \"pl-8\", className)}\n      {...props}\n    >\n      <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n        <Primitive.ItemIndicator>\n          <Check aria-hidden=\"true\" />\n        </Primitive.ItemIndicator>\n      </span>\n      {children}\n    </Primitive.CheckboxItem>\n  );\n}\nexport function DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: ComponentProps<typeof Primitive.RadioItem>) {\n  return (\n    <Primitive.RadioItem\n      data-slot=\"dropdown-item\"\n      className={cn(menuItemClass, \"pl-8\", className)}\n      {...props}\n    >\n      <span className=\"absolute left-2.5 flex size-4 items-center justify-center\">\n        <Primitive.ItemIndicator>\n          <Circle className=\"!size-2 fill-current\" aria-hidden=\"true\" />\n        </Primitive.ItemIndicator>\n      </span>\n      {children}\n    </Primitive.RadioItem>\n  );\n}\nexport function DropdownMenuLabel({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Label>) {\n  return (\n    <Primitive.Label\n      className={cn(\n        \"px-2.5 pb-1 pt-2 text-xs font-medium text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\nexport function DropdownMenuSeparator({\n  className,\n  ...props\n}: ComponentProps<typeof Primitive.Separator>) {\n  return (\n    <Primitive.Separator\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function DropdownMenuShortcut({\n  className,\n  ...props\n}: ComponentProps<\"span\">) {\n  return (\n    <span\n      className={cn(\n        \"ml-auto pl-4 font-mono text-xs tracking-wide text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/**\n * A destructive item that asks in place: choosing it turns the row into the\n * question with Cancel and the confirming action, instead of stacking a\n * dialog on the menu. Both answers are menu items, so arrows and Enter work.\n */\nexport function DropdownMenuConfirmItem({\n  label,\n  icon,\n  question,\n  detail,\n  confirmLabel = \"Delete\",\n  onConfirm,\n}: {\n  label: string;\n  icon?: ReactNode;\n  /** e.g. \"Delete 3 files?\" */\n  question: string;\n  /** e.g. \"They move to Trash for 30 days.\" */\n  detail?: string;\n  confirmLabel?: string;\n  onConfirm: () => void;\n}) {\n  const [asking, setAsking] = useState(false);\n  const cancel = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    if (asking) cancel.current?.focus();\n  }, [asking]);\n  if (!asking) {\n    return (\n      <DropdownMenuItem\n        data-variant=\"destructive\"\n        onSelect={(event) => {\n          event.preventDefault();\n          setAsking(true);\n        }}\n      >\n        {icon}\n        {label}\n      </DropdownMenuItem>\n    );\n  }\n  return (\n    <div data-slot=\"menu-confirm\" role=\"group\" aria-label={question}>\n      <p className=\"px-2.5 pt-2 text-[13px] font-medium text-destructive\">{question}</p>\n      {detail && <p className=\"px-2.5 pb-1 text-xs text-muted-foreground\">{detail}</p>}\n      <DropdownMenuItem\n        ref={cancel}\n        onSelect={(event) => {\n          event.preventDefault();\n          setAsking(false);\n        }}\n      >\n        Cancel\n      </DropdownMenuItem>\n      <DropdownMenuItem data-variant=\"destructive\" onSelect={onConfirm}>\n        {icon}\n        {confirmLabel}\n      </DropdownMenuItem>\n    </div>\n  );\n}\n"},{"path":"ui/glide-highlight.tsx","type":"registry:ui","target":"components/ui/glide-highlight.tsx","content":"\"use client\";\n\nimport { useLayoutEffect, useState } from \"react\";\nimport \"./glide-highlight.css\";\n\nexport type GlideBox = {\n  active: boolean;\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n};\n\nexport function glideBox(target: HTMLElement): GlideBox {\n  return {\n    active: true,\n    left: target.offsetLeft,\n    top: target.offsetTop,\n    width: target.offsetWidth,\n    height: target.offsetHeight,\n  };\n}\n\nexport function useHighlightedGlide(\n  root: HTMLElement | null,\n  selector = \"[data-highlighted]\",\n  /** Attributes whose change can move the target, e.g. data-state for an open trigger. */\n  attributes: readonly string[] = [\"data-highlighted\"],\n) {\n  const [box, setBox] = useState<GlideBox | null>(null);\n\n  useLayoutEffect(() => {\n    const element = root;\n    if (!element) return;\n    const update = () => {\n      const target = element.querySelector<HTMLElement>(selector);\n      setBox((current) =>\n        target ? glideBox(target) : current ? { ...current, active: false } : null,\n      );\n    };\n    update();\n    const observer = new MutationObserver(update);\n    observer.observe(element, {\n      subtree: true,\n      attributes: true,\n      attributeFilter: [...attributes],\n    });\n    return () => observer.disconnect();\n    // The attribute list is fixed per call site.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [root, selector]);\n\n  return box;\n}\n\nexport function GlideHighlight({ box }: { box: GlideBox | null }) {\n  if (!box) return null;\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"glide-highlight\"\n      data-active={box.active}\n      style={{\n        width: box.width,\n        height: box.height,\n        transform: `translate(${box.left}px, ${box.top}px)`,\n      }}\n    />\n  );\n}\n"},{"path":"ui/kbd.tsx","type":"registry:ui","target":"components/ui/kbd.tsx","content":"\"use client\";\n\nimport { useSyncExternalStore, type ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./kbd.css\";\n\nexport type KbdPlatform = \"mac\" | \"other\";\n\n/* Each key: the glyph people see on that platform, and the word a screen reader says. */\nconst named: Record<string, { mac: string; other: string; spoken: string; aria: string }> = {\n  mod: { mac: \"⌘\", other: \"Ctrl\", spoken: \"Command\", aria: \"Meta\" },\n  cmd: { mac: \"⌘\", other: \"⌘\", spoken: \"Command\", aria: \"Meta\" },\n  meta: { mac: \"⌘\", other: \"Win\", spoken: \"Command\", aria: \"Meta\" },\n  ctrl: { mac: \"⌃\", other: \"Ctrl\", spoken: \"Control\", aria: \"Control\" },\n  alt: { mac: \"⌥\", other: \"Alt\", spoken: \"Option\", aria: \"Alt\" },\n  option: { mac: \"⌥\", other: \"Alt\", spoken: \"Option\", aria: \"Alt\" },\n  shift: { mac: \"⇧\", other: \"Shift\", spoken: \"Shift\", aria: \"Shift\" },\n  enter: { mac: \"↵\", other: \"Enter\", spoken: \"Enter\", aria: \"Enter\" },\n  return: { mac: \"↵\", other: \"Enter\", spoken: \"Return\", aria: \"Enter\" },\n  esc: { mac: \"Esc\", other: \"Esc\", spoken: \"Escape\", aria: \"Escape\" },\n  escape: { mac: \"Esc\", other: \"Esc\", spoken: \"Escape\", aria: \"Escape\" },\n  tab: { mac: \"⇥\", other: \"Tab\", spoken: \"Tab\", aria: \"Tab\" },\n  backspace: { mac: \"⌫\", other: \"Backspace\", spoken: \"Backspace\", aria: \"Backspace\" },\n  delete: { mac: \"⌦\", other: \"Del\", spoken: \"Delete\", aria: \"Delete\" },\n  space: { mac: \"Space\", other: \"Space\", spoken: \"Space\", aria: \"Space\" },\n  up: { mac: \"↑\", other: \"↑\", spoken: \"Up arrow\", aria: \"ArrowUp\" },\n  down: { mac: \"↓\", other: \"↓\", spoken: \"Down arrow\", aria: \"ArrowDown\" },\n  left: { mac: \"←\", other: \"←\", spoken: \"Left arrow\", aria: \"ArrowLeft\" },\n  right: { mac: \"→\", other: \"→\", spoken: \"Right arrow\", aria: \"ArrowRight\" },\n  pageup: { mac: \"PgUp\", other: \"PgUp\", spoken: \"Page up\", aria: \"PageUp\" },\n  pagedown: { mac: \"PgDn\", other: \"PgDn\", spoken: \"Page down\", aria: \"PageDown\" },\n  home: { mac: \"Home\", other: \"Home\", spoken: \"Home\", aria: \"Home\" },\n  end: { mac: \"End\", other: \"End\", spoken: \"End\", aria: \"End\" },\n};\n\n/** \"mod+shift+k\" or [\"mod\", \"K\"] → [\"mod\", \"shift\", \"k\"]. A lone \"+\" stays a key. */\nfunction tokens(keys: string | readonly string[]) {\n  if (typeof keys !== \"string\") return [...keys];\n  if (keys === \"+\") return [\"+\"];\n  return keys.split(/\\+(?!$)/u).map((key) => key.trim()).filter(Boolean);\n}\n\nfunction describe(key: string, platform: KbdPlatform) {\n  const known = named[key.toLowerCase()];\n  if (known) return { glyph: known[platform], spoken: known.spoken, aria: known.aria };\n  const glyph = key.length === 1 ? key.toUpperCase() : key;\n  return { glyph, spoken: glyph, aria: glyph };\n}\n\nfunction detect(): KbdPlatform {\n  const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n  const name = nav.userAgentData?.platform ?? nav.platform ?? \"\";\n  return /mac|iphone|ipad|ipod/iu.test(name) ? \"mac\" : \"other\";\n}\n\n/** The platform's modifier names. The server renders \"other\"; Macs switch to ⌘ after hydration. */\nexport function useKbdPlatform(platform?: KbdPlatform): KbdPlatform {\n  const detected = useSyncExternalStore(\n    () => () => {},\n    detect,\n    () => \"other\" as const,\n  );\n  return platform ?? detected;\n}\n\n/** The value for `aria-keyshortcuts` on the control the keys trigger, e.g. \"Meta+K\". */\nexport function toAriaKeyshortcuts(keys: string | readonly string[], platform: KbdPlatform = \"other\") {\n  return tokens(keys)\n    .map((key) => {\n      if (key.toLowerCase() === \"mod\") return platform === \"mac\" ? \"Meta\" : \"Control\";\n      return describe(key, platform).aria;\n    })\n    .join(\"+\");\n}\n\nexport type KbdProps = Omit<ComponentProps<\"kbd\">, \"children\"> & {\n  /** One key or a combination: \"mod+k\", \"shift+enter\", [\"alt\", \"↑\"]. `mod` is ⌘ on a Mac and Ctrl elsewhere. */\n  keys?: string | readonly string[];\n  /** Free text instead of keys, e.g. \"G then D\". */\n  children?: string;\n  /** `inline` sits quietly inside a button, menu row or tooltip and takes its colour. */\n  variant?: \"default\" | \"inline\";\n  size?: \"sm\" | \"md\";\n  /** Force a platform instead of detecting it, e.g. in documentation. */\n  platform?: KbdPlatform;\n};\n\n/**\n * Keys drawn as small raised caps. A combination is one `kbd` holding a cap\n * per key, so it reads as a single shortcut; symbols such as ⌘ carry their\n * spoken name for screen readers.\n */\nexport function Kbd({ keys, children, variant = \"default\", size = \"md\", platform, className, ...props }: KbdProps) {\n  const resolved = useKbdPlatform(platform);\n  const list = keys !== undefined ? tokens(keys) : children !== undefined ? [children] : [];\n  const parts = list.map((key) => describe(key, resolved));\n  const shared = { \"data-variant\": variant, \"data-size\": size } as const;\n  if (parts.length === 1) {\n    return (\n      <kbd data-slot=\"kbd\" {...shared} className={className} {...props}>\n        <Glyph {...parts[0]!} />\n      </kbd>\n    );\n  }\n  return (\n    <kbd data-slot=\"kbd-group\" {...shared} className={cn(className)} {...props}>\n      {parts.map((part, index) => (\n        <kbd key={index} data-slot=\"kbd\" {...shared}>\n          {index > 0 && <span className=\"sr-only\"> </span>}\n          <Glyph {...part} />\n        </kbd>\n      ))}\n    </kbd>\n  );\n}\n\nfunction Glyph({ glyph, spoken }: { glyph: string; spoken: string }) {\n  if (glyph === spoken) return glyph;\n  return (\n    <>\n      <span aria-hidden=\"true\">{glyph}</span>\n      <span className=\"sr-only\">{spoken}</span>\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/toolbar.tsx","type":"registry:ui","target":"components/ui/toolbar.tsx","content":"\"use client\";\n\nimport { Ellipsis } from \"lucide-react\";\nimport {\n  createContext,\n  Fragment,\n  useCallback,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { Button } from \"./button\";\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport { GlideHighlight, glideBox, type GlideBox } from \"./glide-highlight\";\nimport { Kbd } from \"./kbd\";\nimport { cn } from \"./lib/cn\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"./tooltip\";\nimport \"./toolbar.css\";\n\n/* How each tool appears in the More menu once it no longer fits. */\ntype Entry =\n  | { kind: \"action\"; label: string; icon?: ReactNode; disabled?: boolean; onSelect: () => void }\n  | { kind: \"toggle\"; label: string; icon?: ReactNode; disabled?: boolean; checked: boolean; onSelect: () => void }\n  | { kind: \"option\"; group: string; groupLabel: string; value: string; label: string; icon?: ReactNode; checked: boolean; onSelect: () => void };\n\ntype Ctx = { orientation: \"horizontal\" | \"vertical\"; hidden: ReadonlySet<string>; entries: Map<string, Entry> };\ntype Group = { id: string; label: string; type: \"single\" | \"multiple\"; values: readonly string[]; toggle: (value: string) => void };\n\nconst ToolbarContext = createContext<Ctx | null>(null);\nconst GroupContext = createContext<Group | null>(null);\n\n/** Registers a tool's menu form after each render, so the menu always has the latest state. */\nfunction useEntry(entry: Entry) {\n  const ctx = useContext(ToolbarContext);\n  const id = useId();\n  useLayoutEffect(() => {\n    if (!ctx) return;\n    ctx.entries.set(id, entry);\n    return () => void ctx.entries.delete(id);\n  });\n  return id;\n}\n\n/** A piece of the row that folds as one: a tool, a group or a separator. */\nfunction useUnit() {\n  const ctx = useContext(ToolbarContext);\n  const id = useId();\n  return { id, hidden: ctx?.hidden.has(id) ?? false, orientation: ctx?.orientation ?? \"horizontal\" };\n}\n\nconst MORE = 36;\nconst focusables = \"button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], [tabindex]:not([tabindex='-1'])\";\n\n/**\n * A row of tools with one tab stop. Arrows, Home and End move between them,\n * a toggled tool sits recessed in the shell, and when the row runs out of\n * width the last tools fold into a More menu that keeps their labels, state\n * and order. Fields inside keep their own arrow keys.\n */\nexport function Toolbar({\n  label,\n  orientation = \"horizontal\",\n  overflow = true,\n  className,\n  children,\n  ...props\n}: Omit<ComponentProps<\"div\">, \"role\"> & {\n  /** Names the toolbar, e.g. \"Formatting\". */\n  label: string;\n  orientation?: \"horizontal\" | \"vertical\";\n  /** Fold tools that do not fit into a More menu (horizontal only). */\n  overflow?: boolean;\n}) {\n  const root = useRef<HTMLDivElement>(null);\n  const widths = useRef(new Map<string, number>());\n  const [entries] = useState(() => new Map<string, Entry>());\n  const [hidden, setHidden] = useState<ReadonlySet<string>>(() => new Set());\n  const [menu, setMenu] = useState<Entry[][]>([]);\n  const [box, setBox] = useState<GlideBox | null>(null);\n  const folds = overflow && orientation === \"horizontal\";\n\n  const items = useCallback(() => {\n    const element = root.current;\n    if (!element) return [];\n    return [...element.querySelectorAll<HTMLElement>(\"[data-toolbar-item], [data-toolbar-generic]\")]\n      .filter((node) => !node.closest(\"[data-overflowed]\"))\n      .map((node) => (node.hasAttribute(\"data-toolbar-item\") ? node : node.querySelector<HTMLElement>(focusables)))\n      .filter((node): node is HTMLElement => !!node && !(node as HTMLButtonElement).disabled);\n  }, []);\n\n  const activate = useCallback((node: HTMLElement) => {\n    for (const other of items()) {\n      other.tabIndex = other === node ? 0 : -1;\n      if (other !== node) delete other.dataset.toolbarActive;\n    }\n    node.dataset.toolbarActive = \"\";\n  }, [items]);\n\n  // One tab stop: the tool used last, or the first one.\n  useLayoutEffect(() => {\n    const list = items();\n    if (!list.length) return;\n    const current = list.find((node) => node.dataset.toolbarActive !== undefined) ?? list[0]!;\n    for (const node of list) node.tabIndex = node === current ? 0 : -1;\n  });\n\n  const fit = useCallback(() => {\n    const element = root.current;\n    if (!element || !folds) return;\n    const units = [...element.querySelectorAll<HTMLElement>(\"[data-toolbar-unit]\")];\n    for (const unit of units) if (!unit.hasAttribute(\"data-overflowed\")) widths.current.set(unit.dataset.toolbarUnit!, unit.offsetWidth + 2);\n    const width = (unit: HTMLElement) => widths.current.get(unit.dataset.toolbarUnit!) ?? 0;\n    const available = element.clientWidth - 6;\n    const next = new Set<string>();\n    if (units.reduce((sum, unit) => sum + width(unit), 0) > available) {\n      let budget = available - MORE - units.filter((unit) => unit.dataset.pinned !== undefined).reduce((sum, unit) => sum + width(unit), 0);\n      let full = false;\n      for (const unit of units) {\n        if (unit.dataset.pinned !== undefined) continue;\n        if (!full && width(unit) <= budget) budget -= width(unit);\n        else {\n          full = true;\n          next.add(unit.dataset.toolbarUnit!);\n        }\n      }\n      // No separator left dangling at the end of the row.\n      for (let i = units.length - 1; i >= 0; i -= 1) {\n        const unit = units[i]!;\n        if (next.has(unit.dataset.toolbarUnit!) || unit.dataset.pinned !== undefined) continue;\n        if (unit.dataset.separator !== undefined) next.add(unit.dataset.toolbarUnit!);\n        else break;\n      }\n    }\n    setHidden((current) => (current.size === next.size && [...next].every((id) => current.has(id)) ? current : next));\n  }, [folds]);\n\n  useLayoutEffect(() => {\n    const element = root.current;\n    if (!element || !folds) return;\n    fit();\n    const observer = new ResizeObserver(fit);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [fit, folds]);\n\n  function move(event: KeyboardEvent<HTMLDivElement>) {\n    const target = event.target as HTMLElement;\n    if (target.matches(\"input, textarea, [contenteditable='true']\")) return;\n    const back = orientation === \"horizontal\" ? \"ArrowLeft\" : \"ArrowUp\";\n    const forward = orientation === \"horizontal\" ? \"ArrowRight\" : \"ArrowDown\";\n    if (![back, forward, \"Home\", \"End\"].includes(event.key)) return;\n    const list = items();\n    const index = list.findIndex((node) => node === target || node.contains(target));\n    if (index < 0) return;\n    event.preventDefault();\n    const next = event.key === \"Home\" ? 0 : event.key === \"End\" ? list.length - 1 : (index + (event.key === forward ? 1 : -1) + list.length) % list.length;\n    activate(list[next]!);\n    list[next]!.focus();\n  }\n\n  function place(target: EventTarget | null) {\n    const button = target instanceof Element ? target.closest<HTMLElement>('[data-slot=\"button\"]') : null;\n    if (!button || !root.current?.contains(button)) return;\n    setBox(glideBox(button));\n  }\n  const rest = () => setBox((current) => (current ? { ...current, active: false } : null));\n\n  function openMenu(open: boolean) {\n    if (!open || !root.current) return;\n    // Units in row order, each with its tools in order.\n    const units = [...root.current.querySelectorAll<HTMLElement>(\"[data-toolbar-unit][data-overflowed]\")];\n    setMenu(\n      units\n        .map((unit) => [...unit.querySelectorAll<HTMLElement>(\"[data-toolbar-entry]\")].map((node) => entries.get(node.dataset.toolbarEntry!)).filter((entry): entry is Entry => !!entry))\n        .filter((list) => list.length > 0),\n    );\n  }\n\n  return (\n    <ToolbarContext.Provider value={{ orientation, hidden, entries }}>\n      <TooltipProvider>\n        <div\n          {...props}\n          ref={root}\n          role=\"toolbar\"\n          aria-label={label}\n          aria-orientation={orientation}\n          data-slot=\"toolbar\"\n          data-orientation={orientation}\n          onKeyDown={(event) => {\n            move(event);\n            props.onKeyDown?.(event);\n          }}\n          onFocus={(event) => {\n            const node = items().find((item) => item === event.target || item.contains(event.target as Node));\n            if (node) activate(node);\n            place(event.target);\n          }}\n          onBlur={(event) => !root.current?.contains(event.relatedTarget as Node) && rest()}\n          onPointerMove={(event) => place(event.target)}\n          onPointerLeave={rest}\n          className={cn(\n            \"relative flex max-w-full min-w-0 gap-0.5 rounded-touch bg-raised p-[3px] shadow-raised\",\n            orientation === \"vertical\" ? \"inline-flex flex-col\" : \"items-center\",\n            className,\n          )}\n        >\n          <GlideHighlight box={box} />\n          {children}\n          {folds && hidden.size > 0 && (\n            <DropdownMenu onOpenChange={openMenu}>\n              <DropdownMenuTrigger asChild>\n                <Button variant=\"ghost\" size=\"icon\" data-toolbar-item=\"\" aria-label=\"More tools\" className=\"ml-auto\">\n                  <Ellipsis aria-hidden=\"true\" />\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent align=\"end\">\n                {menu.map((list, index) => (\n                  <Fragment key={index}>\n                    {/* Single tools sit together; groups get a rule either side. */}\n                    {index > 0 && (list.length > 1 || menu[index - 1]!.length > 1) && <DropdownMenuSeparator />}\n                    <MenuUnit entries={list} />\n                  </Fragment>\n                ))}\n              </DropdownMenuContent>\n            </DropdownMenu>\n          )}\n        </div>\n      </TooltipProvider>\n    </ToolbarContext.Provider>\n  );\n}\n\nfunction MenuUnit({ entries }: { entries: Entry[] }) {\n  const first = entries[0]!;\n  if (first.kind === \"option\") {\n    const chosen = entries.find((entry) => entry.kind === \"option\" && entry.checked);\n    return (\n      <>\n        <DropdownMenuLabel>{first.groupLabel}</DropdownMenuLabel>\n        <DropdownMenuRadioGroup value={chosen?.kind === \"option\" ? chosen.value : \"\"}>\n          {entries.map((entry) =>\n            entry.kind === \"option\" ? (\n              <DropdownMenuRadioItem key={entry.value} value={entry.value} onSelect={entry.onSelect}>\n                {entry.icon}\n                {entry.label}\n              </DropdownMenuRadioItem>\n            ) : null,\n          )}\n        </DropdownMenuRadioGroup>\n      </>\n    );\n  }\n  return entries.map((entry) =>\n    entry.kind === \"toggle\" ? (\n      <DropdownMenuCheckboxItem key={entry.label} checked={entry.checked} disabled={entry.disabled} onSelect={entry.onSelect}>\n        {entry.label}\n      </DropdownMenuCheckboxItem>\n    ) : entry.kind === \"action\" ? (\n      <DropdownMenuItem key={entry.label} disabled={entry.disabled} onSelect={entry.onSelect}>\n        {entry.icon}\n        {entry.label}\n      </DropdownMenuItem>\n    ) : null,\n  );\n}\n\nfunction ToolTip({ label, shortcut, show, children }: { label: string; shortcut?: string; show: boolean; children: ReactNode }) {\n  if (!show && !shortcut) return children;\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>{children}</TooltipTrigger>\n      <TooltipContent>\n        {label}\n        {shortcut && <Kbd keys={shortcut} variant=\"inline\" size=\"sm\" />}\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\n/**\n * One tool. With an icon and no `showLabel`, the label becomes its accessible\n * name and tooltip. Pass `pressed` to make it a toggle such as Bold.\n */\nexport function ToolbarButton({\n  label,\n  icon,\n  showLabel = !icon,\n  shortcut,\n  pressed,\n  onPressedChange,\n  onClick,\n  pinned = false,\n  disabled,\n  className,\n}: {\n  label: string;\n  icon?: ReactNode;\n  showLabel?: boolean;\n  /** Shown in the tooltip, e.g. \"mod+b\". Your app listens for the keys. */\n  shortcut?: string;\n  pressed?: boolean;\n  onPressedChange?: (pressed: boolean) => void;\n  onClick?: () => void;\n  /** Never folds into the More menu. */\n  pinned?: boolean;\n  disabled?: boolean;\n  className?: string;\n}) {\n  const toggle = pressed !== undefined;\n  const activate = () => (toggle ? onPressedChange?.(!pressed) : onClick?.());\n  const entry = useEntry(toggle ? { kind: \"toggle\", label, icon, disabled, checked: !!pressed, onSelect: activate } : { kind: \"action\", label, icon, disabled, onSelect: activate });\n  const unit = useUnit();\n  return (\n    <span data-toolbar-unit={unit.id} data-pinned={pinned ? \"\" : undefined} data-overflowed={unit.hidden ? \"\" : undefined}>\n      <ToolTip label={label} shortcut={shortcut} show={!showLabel}>\n        <Button\n          variant=\"ghost\"\n          size={showLabel ? \"sm\" : \"icon\"}\n          data-toolbar-item=\"\"\n          data-toolbar-entry={entry}\n          aria-pressed={toggle ? pressed : undefined}\n          aria-label={showLabel ? undefined : label}\n          disabled={disabled}\n          onClick={activate}\n          className={className}\n        >\n          {icon}\n          {showLabel && label}\n        </Button>\n      </ToolTip>\n    </span>\n  );\n}\n\n/**\n * Tools that belong together and fold together: `single` is one choice such\n * as alignment (a radio group), `multiple` is independent toggles such as\n * bold and italic.\n */\nexport function ToolbarToggleGroup({\n  label,\n  pinned = false,\n  children,\n  ...choice\n}: {\n  label: string;\n  pinned?: boolean;\n  children: ReactNode;\n} & (\n  | { type: \"single\"; value: string; onValueChange: (value: string) => void }\n  | { type: \"multiple\"; value: readonly string[]; onValueChange: (value: string[]) => void }\n)) {\n  const { type } = choice;\n  const unit = useUnit();\n  const values = choice.type === \"single\" ? (choice.value ? [choice.value] : []) : choice.value;\n  const toggle = (item: string) => {\n    if (choice.type === \"single\") choice.onValueChange(item);\n    else choice.onValueChange(values.includes(item) ? values.filter((v) => v !== item) : [...values, item]);\n  };\n  return (\n    <GroupContext.Provider value={{ id: unit.id, label, type, values, toggle }}>\n      <span\n        role={type === \"single\" ? \"radiogroup\" : \"group\"}\n        aria-label={label}\n        data-slot=\"toolbar-group\"\n        data-toolbar-unit={unit.id}\n        data-pinned={pinned ? \"\" : undefined}\n        data-overflowed={unit.hidden ? \"\" : undefined}\n      >\n        {children}\n      </span>\n    </GroupContext.Provider>\n  );\n}\n\nexport function ToolbarToggleItem({ value, label, icon, shortcut, disabled }: { value: string; label: string; icon?: ReactNode; shortcut?: string; disabled?: boolean }) {\n  const group = useContext(GroupContext);\n  if (!group) throw new Error(\"ToolbarToggleItem belongs inside a ToolbarToggleGroup.\");\n  const on = group.values.includes(value);\n  const single = group.type === \"single\";\n  const select = () => group.toggle(value);\n  const entry = useEntry(\n    single\n      ? { kind: \"option\", group: group.id, groupLabel: group.label, value, label, icon, checked: on, onSelect: select }\n      : { kind: \"toggle\", label, icon, disabled, checked: on, onSelect: select },\n  );\n  return (\n    <ToolTip label={label} shortcut={shortcut} show={!!icon}>\n      <Button\n        variant=\"ghost\"\n        size={icon ? \"icon\" : \"sm\"}\n        data-toolbar-item=\"\"\n        data-toolbar-entry={entry}\n        role={single ? \"radio\" : undefined}\n        aria-checked={single ? on : undefined}\n        aria-pressed={single ? undefined : on}\n        data-on={on || undefined}\n        aria-label={icon ? label : undefined}\n        disabled={disabled}\n        onClick={select}\n      >\n        {icon ?? label}\n      </Button>\n    </ToolTip>\n  );\n}\n\nexport function ToolbarSeparator() {\n  const unit = useUnit();\n  return (\n    <span\n      role=\"separator\"\n      aria-orientation={unit.orientation === \"vertical\" ? \"horizontal\" : \"vertical\"}\n      data-slot=\"toolbar-separator\"\n      data-toolbar-unit={unit.id}\n      data-separator=\"\"\n      data-overflowed={unit.hidden ? \"\" : undefined}\n    />\n  );\n}\n\n/**\n * Any other control in the row, such as a SelectMenu or a zoom field. It\n * joins the arrow-key order and stays visible when the row folds.\n */\nexport function ToolbarItem({ children, className }: { children: ReactNode; className?: string }) {\n  const unit = useUnit();\n  return (\n    <span data-toolbar-unit={unit.id} data-toolbar-generic=\"\" data-pinned=\"\" className={className}>\n      {children}\n    </span>\n  );\n}\n"},{"path":"ui/tooltip.tsx","type":"registry:ui","target":"components/ui/tooltip.tsx","content":"\"use client\";\n\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport type { ComponentProps } from \"react\";\nimport { cn } from \"./lib/cn\";\nimport \"./tooltip.css\";\n\nexport const Tooltip = TooltipPrimitive.Root;\nexport const TooltipTrigger = TooltipPrimitive.Trigger;\n\n/**\n * The first tooltip waits 300ms; neighbours then show at once for 500ms.\n * Tooltips are labels, not panels, so their content is not hoverable: moving\n * along a toolbar switches straight to the next label.\n */\nexport function TooltipProvider({\n  delayDuration = 300,\n  skipDelayDuration = 500,\n  disableHoverableContent = true,\n  ...props\n}: ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return (\n    <TooltipPrimitive.Provider\n      delayDuration={delayDuration}\n      skipDelayDuration={skipDelayDuration}\n      disableHoverableContent={disableHoverableContent}\n      {...props}\n    />\n  );\n}\n\nexport function TooltipContent({\n  className,\n  sideOffset = 6,\n  shortcut,\n  children,\n  ...props\n}: ComponentProps<typeof TooltipPrimitive.Content> & {\n  /** Keyboard shortcut shown after the label, e.g. \"⌘D\". */\n  shortcut?: string;\n}) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"z-50 inline-flex max-w-60 items-center gap-2 rounded-control bg-foreground px-2.5 py-1.5 text-xs leading-4 font-medium text-background shadow-feedback\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n        {shortcut && <kbd data-slot=\"tooltip-shortcut\">{shortcut}</kbd>}\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\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/shared/dropdown-content.css","type":"registry:file","target":"components/ui/shared/dropdown-content.css","content":"/* Shared by context-menu.tsx, dropdown-menu.tsx; each imports it. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"dropdown-item\"] {\n    transition:\n      color var(--duration-fast) var(--ease-out),\n      background-color var(--duration-fast) var(--ease-out),\n      scale var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"dropdown-item\"]:focus-visible {\n    outline: 2px solid var(--ring);\n    outline-offset: -2px;\n  }\n  [data-slot=\"dropdown-content\"] {\n    transform-origin: var(--radix-dropdown-menu-content-transform-origin);\n  }\n  [data-slot=\"dropdown-content\"] [data-slot=\"glide-highlight\"] {\n    border-radius: var(--radius-control);\n    transition:\n      transform var(--duration-fast) var(--ease-out),\n      width var(--duration-fast) var(--ease-out),\n      height var(--duration-fast) var(--ease-out),\n      opacity var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"dropdown-content\"][data-state=\"open\"] {\n    animation: caliper-dialog-enter var(--duration-normal) var(--ease-settle);\n  }\n  [data-slot=\"dropdown-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=\"dropdown-item\"] {\n      transition: none;\n    }\n    [data-slot=\"dropdown-content\"] {\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=\"dropdown-content\"][data-state=\"closed\"] {\n      animation: caliper-fade-out var(--duration-instant) var(--ease-in) forwards !important;\n    }\n  }\n}\n\n[data-slot=\"dropdown-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@layer components {\n  [data-slot=\"dropdown-content\"][data-variant=\"glass\"] {\n    background: var(--elevated-glass);\n    -webkit-backdrop-filter: blur(24px) saturate(1.5);\n    backdrop-filter: blur(24px) saturate(1.5);\n    border-color: color-mix(in srgb, var(--edge-light) 45%, var(--border));\n    box-shadow: var(--shadow-overlay), inset 0 1px 0 var(--edge-light);\n    border-radius: 20px;\n  }\n  /* Moving along the bar swaps menus at once: a closing menu that lingered to fade would see focus leave and dismiss the bar. */\n  [data-slot=\"dropdown-content\"][data-menubar][data-state=\"closed\"] { animation: none; }\n}\n"},{"path":"ui/dropdown-menu.css","type":"registry:file","target":"components/ui/dropdown-menu.css","content":"/* dropdown-menu.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"menu-confirm\"] {\n    margin: 2px 0;\n    padding-bottom: 2px;\n    border-radius: var(--radius-control);\n    background: var(--destructive-subtle);\n    animation: caliper-fade-in var(--duration-fast) var(--ease-out);\n  }\n}\n"},{"path":"ui/glide-highlight.css","type":"registry:file","target":"components/ui/glide-highlight.css","content":"/* glide-highlight.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  [data-slot=\"glide-highlight\"] {\n    position: absolute;\n    top: 0;\n    left: 0;\n    z-index: 0;\n    pointer-events: none;\n    border-radius: var(--radius-surface);\n    background: var(--muted);\n    opacity: 0;\n    transition:\n      transform var(--duration-travel) var(--ease-showcase),\n      width var(--duration-travel) var(--ease-showcase),\n      height var(--duration-travel) var(--ease-showcase),\n      opacity var(--duration-deliberate) var(--ease-out);\n  }\n  [data-slot=\"glide-highlight\"][data-active=\"true\"] {\n    opacity: 1;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    [data-slot=\"glide-highlight\"] {\n      transition: none;\n    }\n    [data-slot=\"glide-highlight\"] {\n      transition:\n        transform 400ms var(--ease-showcase),\n        width 400ms var(--ease-showcase),\n        height 400ms var(--ease-showcase),\n        opacity 280ms var(--ease-out);\n    }\n  }\n}\n"},{"path":"ui/kbd.css","type":"registry:file","target":"components/ui/kbd.css","content":"/* kbd.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Key caps: a small raised object with a lip on the bottom edge. */\n  [data-slot=\"kbd-group\"] { display: inline-flex; align-items: center; gap: 3px; font: inherit; }\n  [data-slot=\"kbd\"] {\n    display: inline-flex; align-items: center; justify-content: center;\n    min-width: 20px; height: 20px; padding: 0 5px;\n    border-radius: var(--radius-mark);\n    background: var(--raised);\n    box-shadow: inset 0 0 0 1px var(--border), inset 0 -1px 0 var(--border-strong);\n    color: var(--muted-foreground);\n    font-family: var(--font-mono, ui-monospace, monospace); font-size: 12px; line-height: 1; font-weight: 500;\n    font-variant-numeric: tabular-nums; white-space: nowrap;\n  }\n  /* A key hint inside a search box or field is a soft fill: the box already has the border. */\n  :is([data-slot=\"input-group\"], [data-slot=\"search\"], [data-slot$=\"-search\"], [data-slot=\"command-input\"]) [data-slot=\"kbd\"] { background: var(--muted); box-shadow: none; border: 0; }\n  [data-slot=\"kbd\"][data-size=\"sm\"] { min-width: 18px; height: 18px; padding: 0 4px; font-size: 12px; }\n  [data-slot=\"kbd\"][data-variant=\"inline\"] {\n    background: transparent; color: inherit; opacity: 0.72;\n    box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 26%, transparent);\n  }\n  [data-slot=\"kbd-group\"][data-variant=\"inline\"] { opacity: 1; }\n}\n"},{"path":"ui/toolbar.css","type":"registry:file","target":"components/ui/toolbar.css","content":"/* toolbar.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* Toolbar: the button-group material, with arrow-key focus and a fold. */\n  [data-slot=\"toolbar\"] > [data-slot=\"glide-highlight\"] { border-radius: 8px; transition: transform var(--duration-normal) var(--ease-out), width var(--duration-normal) var(--ease-out), height var(--duration-normal) var(--ease-out), opacity var(--duration-fast) var(--ease-out); }\n  [data-slot=\"toolbar\"] [data-slot=\"button\"] {\n    position: relative; z-index: 1; height: calc(var(--control-height) - 6px); border-radius: 8px;\n    --b-bg: transparent; --b-bg-hover: transparent; --b-bg-press: var(--muted); --b-fg: var(--foreground); --b-shadow: none; --b-shadow-press: none;\n  }\n  [data-slot=\"toolbar\"] [data-slot=\"button\"][data-size=\"icon\"] { width: calc(var(--control-height) - 6px); }\n  [data-slot=\"toolbar\"] [data-slot=\"button\"]:is([aria-pressed=\"true\"], [aria-checked=\"true\"], [data-state=\"open\"]) {\n    --b-bg: var(--raised-press); --b-bg-hover: var(--raised-press); --b-bg-press: var(--raised-press); --b-shadow: var(--shadow-sunken); --b-shadow-press: var(--shadow-sunken);\n  }\n  [data-slot=\"toolbar\"] [data-toolbar-generic] button { height: calc(var(--control-height) - 6px); }\n  [data-slot=\"toolbar\"] [data-toolbar-unit] { display: inline-flex; flex: none; gap: 2px; }\n  [data-slot=\"toolbar\"] [data-toolbar-unit][data-overflowed] { display: none; }\n  [data-slot=\"toolbar-separator\"] { flex: none; align-self: stretch; width: 1px; margin: 6px 3px; background: var(--border); }\n  [data-slot=\"toolbar\"][data-orientation=\"vertical\"] [data-slot=\"toolbar-separator\"] { width: auto; height: 1px; margin: 3px 6px; }\n  [data-slot=\"toolbar\"][data-orientation=\"vertical\"] [data-slot=\"toolbar-group\"] { flex-direction: column; }\n  @media (pointer: coarse) {\n    [data-slot=\"toolbar\"] [data-slot=\"button\"] { height: 40px; }\n    [data-slot=\"toolbar\"] [data-slot=\"button\"][data-size=\"icon\"] { width: 40px; }\n  }\n}\n"},{"path":"ui/tooltip.css","type":"registry:file","target":"components/ui/tooltip.css","content":"/* tooltip.tsx's styles; it imports this file. Edit freely. */\n@layer theme, base, components, utilities;\n\n@layer components {\n  /* The first tooltip fades in; while warm, neighbours swap with no animation. */\n  [data-slot=\"tooltip-content\"][data-state=\"delayed-open\"] {\n    animation: caliper-fade-in var(--duration-fast) var(--ease-out);\n  }\n  [data-slot=\"tooltip-shortcut\"] {\n    display: inline-flex;\n    align-items: center;\n    height: 18px;\n    padding: 0 5px;\n    border-radius: 4px;\n    font-family: var(--font-mono);\n    font-size: 12px;\n    font-weight: 500;\n    background: color-mix(in srgb, var(--background) 16%, transparent);\n  }\n\n  [data-slot=\"tooltip-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=\"tooltip-content\"] {\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=\"tooltip-content\"][data-state=\"closed\"] {\n      animation: caliper-fade-out var(--duration-instant) var(--ease-in) forwards !important;\n    }\n  }\n}\n"}],"categories":["actions"],"meta":{"tier":"free","license":"MIT","family":"actions","docs":"/docs/components/toolbar"}}