{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "type": "registry:item",
  "title": "Context Menu",
  "description": "The shadcn default Context Menu visual port backed by GPUI Kit behavior.",
  "files": [
    {
      "path": "registry/theme/primitives.rs",
      "type": "registry:file",
      "target": "~/src/ui/primitives.rs",
      "content": "//! Shared visual recipes between the theme tokens and the components.\n//!\n//! shadcn expresses these as Tailwind class strings; here each one is a function\n//! that applies the same styles to any GPUI element, so every component draws\n//! surfaces, controls, focus, text and images the same way, and a change to a\n//! recipe reaches the whole catalog. The registry lint fails components that\n//! rewrite one of these by hand. Everything takes `&UiTheme` so it works in\n//! `RenderOnce` and `Render` alike without re-reading the global.\n\nuse gpui_icons::{LucideIcon, lucide};\nuse gpui_kit::{\n    AnyElement, Axis, BoxShadow, Div, FontWeight, ImageSource, Img, InteractiveElement,\n    IntoElement, MouseButton, ObjectFit, ParentElement, Pixels, Rgba, SharedString,\n    StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, Window, div, img, px,\n};\n\nuse super::{UiTextStyle, UiTheme};\n\n/// Text sizes from the type scale, each with its line height and the body font.\npub mod text {\n    use super::*;\n\n    /// Applies one step of the type scale and the body font.\n    pub fn style<T: Styled>(element: T, theme: &UiTheme, style: UiTextStyle) -> T {\n        let style = theme.text_style(style);\n        element\n            .font_family(theme.fonts.body.clone())\n            .text_size(style.size)\n            .line_height(style.line_height)\n    }\n    /// Captions, key caps and timestamps.\n    pub fn xs<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.xs)\n    }\n    /// Controls and body copy; the Nova default.\n    pub fn sm<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.sm)\n    }\n    /// Prose and dialog titles.\n    pub fn base<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.base)\n    }\n    /// Section headings.\n    pub fn lg<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.lg)\n    }\n    /// Page headings.\n    pub fn xl<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.xl)\n    }\n    /// A medium-weight heading in the heading font at the given step.\n    pub fn heading<T: Styled>(element: T, theme: &UiTheme, size: UiTextStyle) -> T {\n        style(element, theme, size)\n            .font_family(theme.fonts.heading.clone())\n            .font_weight(FontWeight::MEDIUM)\n    }\n    /// Switches to the monospace font, keeping the current size.\n    pub fn mono<T: Styled>(element: T, theme: &UiTheme) -> T {\n        element.font_family(theme.fonts.mono.clone())\n    }\n    /// Muted supporting text at the small size.\n    pub fn muted<T: Styled>(element: T, theme: &UiTheme) -> T {\n        sm(element, theme).text_color(theme.colors.muted_foreground)\n    }\n}\n\n/// Icon sizes in spacing units, so icons scale with control density.\npub mod icon {\n    use super::*;\n\n    /// 12px: inside badges and tags.\n    pub fn xs(theme: &UiTheme) -> Pixels {\n        theme.space(3.)\n    }\n    /// 14px: inside small buttons and chips.\n    pub fn sm(theme: &UiTheme) -> Pixels {\n        theme.space(3.5)\n    }\n    /// 16px: the default inside controls and menu items.\n    pub fn md(theme: &UiTheme) -> Pixels {\n        theme.space(4.)\n    }\n    /// 20px: list leading icons.\n    pub fn lg(theme: &UiTheme) -> Pixels {\n        theme.space(5.)\n    }\n    /// 24px: empty states and headers.\n    pub fn xl(theme: &UiTheme) -> Pixels {\n        theme.space(6.)\n    }\n}\n\n/// Keyboard focus treatment: shadcn Nova's `ring-3 ring-ring/50` outside the\n/// border, with the border taking the ring color. Apply through\n/// `focus_visible` so pointer presses leave it alone.\npub mod focus {\n    use super::*;\n\n    /// The ring as a box shadow, for elements that draw their own border.\n    pub fn shadow(color: Rgba) -> Vec<BoxShadow> {\n        vec![BoxShadow::new(px(0.), px(0.), color.into()).spread_radius(px(3.))]\n    }\n    /// The focus ring for an ordinary control.\n    pub fn ring(style: StyleRefinement, theme: &UiTheme) -> StyleRefinement {\n        let control = theme.control();\n        style\n            .border_color(theme.colors.ring)\n            .shadow(shadow(control.ring))\n    }\n    /// The focus ring for an invalid control, in the destructive color.\n    pub fn invalid_ring(style: StyleRefinement, theme: &UiTheme) -> StyleRefinement {\n        let control = theme.control();\n        style\n            .border_color(theme.colors.destructive)\n            .shadow(shadow(control.invalid_ring))\n    }\n    /// Picks the valid or invalid ring.\n    pub fn ring_for(style: StyleRefinement, theme: &UiTheme, invalid: bool) -> StyleRefinement {\n        if invalid {\n            invalid_ring(style, theme)\n        } else {\n            ring(style, theme)\n        }\n    }\n    /// Draws the ring on `element` while `focused`, for controls whose focus is\n    /// tracked on a parent (a grouped checkbox or radio draws it on its box).\n    pub fn shown<T: Styled>(element: T, theme: &UiTheme, focused: bool) -> T {\n        shown_for(element, theme, focused, false)\n    }\n    /// [`shown`] with the invalid ring when `invalid`.\n    pub fn shown_for<T: Styled>(element: T, theme: &UiTheme, focused: bool, invalid: bool) -> T {\n        if focused {\n            super::super::apply_style(\n                element,\n                &ring_for(StyleRefinement::default(), theme, invalid),\n            )\n        } else {\n            element\n        }\n    }\n}\n\n/// Disabled and interactive cursor treatment.\npub mod disabled {\n    use super::*;\n\n    /// Nova's `disabled:opacity-50` plus the blocked cursor.\n    pub fn style<T: Styled>(element: T) -> T {\n        element.opacity(0.5).cursor_not_allowed()\n    }\n    /// A pointer cursor when enabled, the disabled treatment otherwise.\n    pub fn interactive<T: Styled>(element: T, disabled: bool) -> T {\n        if disabled {\n            style(element)\n        } else {\n            element.cursor_pointer()\n        }\n    }\n    /// Half opacity while `dimmed`, for content that is unavailable rather than\n    /// a control that is disabled: uploading pictures, disabled list rows.\n    pub fn dim<T: Styled>(element: T, dimmed: bool) -> T {\n        if dimmed {\n            element.opacity(0.5)\n        } else {\n            element\n        }\n    }\n}\n\n/// Elevated surfaces. One rule per kind, so every popover, menu, dialog, card\n/// and tooltip in the catalog shares border, radius, shadow and text defaults.\npub mod surface {\n    use super::*;\n\n    fn popover_base<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::sm(element, theme)\n            .border_1()\n            .border_color(theme.control().surface_border)\n            .bg(theme.colors.popover)\n            .text_color(theme.colors.popover_foreground)\n    }\n    /// Popovers, hover cards, select and combobox lists: `rounded-lg shadow-md`.\n    pub fn popover<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.lg)\n            .shadow(theme.shadows.md.to_vec())\n    }\n    /// Menus and context menus: a popover with Nova's 4px inset.\n    pub fn menu<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover(element, theme).p(theme.space(1.))\n    }\n    /// Dialogs and command palettes: `rounded-xl shadow-lg`.\n    pub fn modal<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.xl)\n            .shadow(theme.shadows.lg.to_vec())\n    }\n    /// A sheet or drawer panel: the modal surface without a radius, which the\n    /// caller rounds on the open edge.\n    pub fn sheet<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme).border_color(theme.colors.border)\n    }\n    /// Toasts: `rounded-2xl` on the popover surface with the plain border.\n    pub fn toast<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.two_xl)\n            .border_color(theme.colors.border)\n            .shadow(theme.shadows.lg.to_vec())\n    }\n    /// Cards and empty states: `rounded-xl border shadow-sm` on the card color.\n    pub fn card<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::sm(element, theme)\n            .rounded(theme.radius.xl)\n            .border_1()\n            .border_color(theme.colors.border)\n            .bg(theme.colors.card)\n            .text_color(theme.colors.card_foreground)\n            .shadow(theme.shadows.sm.to_vec())\n    }\n    /// A floating action bar over content, as on a hovered message.\n    pub fn floating<T: Styled>(element: T, theme: &UiTheme) -> T {\n        element\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(theme.colors.border)\n            .bg(theme.colors.background)\n            .shadow(theme.shadows.sm.to_vec())\n    }\n    /// Tooltips: inverted colors at the small radius and caption size.\n    pub fn tooltip<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::xs(element, theme)\n            .rounded(theme.radius.sm)\n            .px(theme.space(3.))\n            .py(theme.space(1.5))\n            .bg(theme.colors.foreground)\n            .text_color(theme.colors.background)\n    }\n    /// A one-pixel rule in the border color, across or down.\n    pub fn divider<T: Styled>(element: T, theme: &UiTheme, axis: Axis) -> T {\n        let element = match axis {\n            Axis::Horizontal => element.w_full().h(px(1.)),\n            Axis::Vertical => element.h_full().w(px(1.)),\n        };\n        element.bg(theme.colors.border)\n    }\n}\n\n/// Form control pieces shared by single controls and their grouped variants.\npub mod control {\n    use super::*;\n\n    /// Corner radius of a 16px check box: Nova's `rounded-[4px]` at the default radius.\n    pub fn box_radius(theme: &UiTheme) -> Pixels {\n        theme.radius.sm * (2. / 3.)\n    }\n\n    /// The frame every text-like control shares: inputs, textareas, selects,\n    /// number and OTP cells. Large radius, input border and fill, small text.\n    /// Callers set the size: `h(theme.space(8.))` and `px(theme.space(2.5))`\n    /// for a single-line control.\n    pub fn frame<T: Styled>(element: T, theme: &UiTheme, invalid: bool) -> T {\n        let control = theme.control();\n        text::sm(element, theme)\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(if invalid {\n                theme.colors.destructive\n            } else {\n                control.border\n            })\n            .bg(control.background)\n            .text_color(theme.colors.foreground)\n    }\n\n    /// Styles `element` as the 16px check box with its mark, for checkboxes and\n    /// grouped checkboxes. The element is the interactive root of the control.\n    pub fn checkbox_box<T: Styled + ParentElement>(\n        element: T,\n        theme: &UiTheme,\n        checked: bool,\n        indeterminate: bool,\n    ) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let selected = checked || indeterminate;\n        let element = element\n            .flex()\n            .flex_shrink_0()\n            .items_center()\n            .justify_center()\n            .size(theme.space(4.))\n            .rounded(box_radius(theme))\n            .border_1()\n            .border_color(if selected {\n                colors.primary\n            } else {\n                control.border\n            })\n            .bg(if selected {\n                colors.primary\n            } else {\n                control.background\n            });\n        if selected {\n            element.child(\n                lucide(if indeterminate {\n                    LucideIcon::Minus\n                } else {\n                    LucideIcon::Check\n                })\n                .size(icon::sm(theme))\n                .text_color(colors.primary_foreground),\n            )\n        } else {\n            element\n        }\n    }\n\n    /// Styles `element` as the 16px radio circle with its dot.\n    pub fn radio_dot<T: Styled + ParentElement>(element: T, theme: &UiTheme, checked: bool) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let element = element\n            .flex()\n            .flex_shrink_0()\n            .items_center()\n            .justify_center()\n            .size(theme.space(4.))\n            .rounded_full()\n            .border_1()\n            .border_color(if checked {\n                colors.primary\n            } else {\n                control.border\n            })\n            .bg(if checked {\n                colors.primary\n            } else {\n                control.background\n            });\n        if checked {\n            element.child(\n                div()\n                    .size(theme.space(2.))\n                    .rounded_full()\n                    .bg(colors.primary_foreground),\n            )\n        } else {\n            element\n        }\n    }\n\n    /// The toggle look shared by Toggle and Toggle Group items: transparent at\n    /// rest, muted when hovered or pressed, an input border when outlined.\n    pub fn toggle<T: Styled>(\n        element: T,\n        theme: &UiTheme,\n        pressed: bool,\n        disabled: bool,\n        outline: bool,\n    ) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let element = text::sm(element, theme)\n            .border_1()\n            .border_color(if outline {\n                control.border\n            } else {\n                colors.background.opacity(0.)\n            })\n            .text_color(colors.foreground)\n            .bg(if pressed {\n                control.pressed\n            } else {\n                colors.background.opacity(0.)\n            });\n        disabled::interactive(element, disabled)\n    }\n}\n\n/// The switch track and thumb colors for a checked or unchecked switch.\npub fn switch_colors(theme: &UiTheme, checked: bool) -> (Rgba, Rgba) {\n    let control = theme.control();\n    if checked {\n        (theme.colors.primary, control.thumb_checked)\n    } else {\n        (control.track, control.thumb)\n    }\n}\n\n/// An image inside a rounded frame. GPUI clips children to rectangles, so the\n/// frame rounds itself and the image rounds to the frame's inner radius; the\n/// image fills the frame's box and never sizes the frame from its aspect ratio.\npub fn clipped_image(\n    source: impl Into<ImageSource>,\n    radius: Pixels,\n    fit: ObjectFit,\n    theme: &UiTheme,\n) -> Div {\n    clipped_image_with(source, radius, fit, theme, |image| image)\n}\n\n/// [`clipped_image`] with access to the picture, for loading and fallback views.\npub fn clipped_image_with(\n    source: impl Into<ImageSource>,\n    radius: Pixels,\n    fit: ObjectFit,\n    theme: &UiTheme,\n    configure: impl FnOnce(Img) -> Img,\n) -> Div {\n    div()\n        .relative()\n        .flex_shrink_0()\n        .overflow_hidden()\n        .rounded(radius)\n        .bg(theme.colors.muted)\n        .child(configure(\n            img(source)\n                .absolute()\n                .inset_0()\n                .size_full()\n                .rounded(radius)\n                .object_fit(fit),\n        ))\n}\n\n/// A circular image, as in avatars.\npub fn round_image(source: impl Into<ImageSource>, size: Pixels, theme: &UiTheme) -> Div {\n    clipped_image(source, size / 2., ObjectFit::Cover, theme).size(size)\n}\n\n/// Dismisses an overlay from a press outside it or the Escape key.\npub mod dismiss {\n    use super::*;\n\n    /// Runs `handler` when the pointer presses outside `element` or Escape is\n    /// pressed while focus is inside it.\n    pub fn on_outside<E: StatefulInteractiveElement>(\n        element: E,\n        handler: impl Fn(&mut Window, &mut gpui_kit::App) + 'static,\n    ) -> E {\n        let handler = std::rc::Rc::new(handler);\n        let on_press = handler.clone();\n        element\n            .on_mouse_down_out(move |event, window, cx| {\n                if event.button == MouseButton::Left {\n                    on_press(window, cx)\n                }\n            })\n            .on_key_down(move |event, window, cx| {\n                if event.keystroke.key == \"escape\" {\n                    handler(window, cx);\n                    cx.stop_propagation();\n                }\n            })\n    }\n}\n\n/// Hover-revealed content that never changes layout: the element keeps its\n/// size and only its opacity follows `visible`.\npub fn hover_reveal<T: Styled>(element: T, visible: bool) -> T {\n    element.opacity(if visible { 1. } else { 0. })\n}\n\n/// Content shown while the pointer is over the enclosing `.group(name)`\n/// element, with no state and no re-render: opacity 0 at rest, 1 on hover.\n/// Pass `pinned` to keep it visible, as for a selected row's actions.\npub fn hover_reveal_in_group<T: Styled + InteractiveElement>(\n    element: T,\n    group: impl Into<SharedString>,\n    pinned: bool,\n) -> T {\n    if pinned {\n        element\n    } else {\n        element\n            .opacity(0.)\n            .group_hover(group, |style| style.opacity(1.))\n    }\n}\n\n/// Erases an element's type after styling, for callers that store children.\npub fn erase(element: impl IntoElement) -> AnyElement {\n    element.into_any_element()\n}\n"
    },
    {
      "path": "registry/theme/theme.rs",
      "type": "registry:file",
      "target": "~/src/ui/theme.rs",
      "content": "//! The app-owned shadcn Neutral theme for gpuicn.\n//!\n//! Source: shadcn/ui 4.19.0 at\n//! `1773ecfeeb4a04366978d353e69b5c7ded78dcb2`, Nova style.\n\nuse std::{sync::Arc, time::Duration};\n\nuse gpui_kit::{\n    App, BoxShadow, ElementId, Global, Pixels, Rgba, SharedString, Styled, Subscription, Window,\n    WindowAppearance, black, px,\n};\n\n#[path = \"primitives.rs\"]\npub mod primitives;\n\ngpui_kit::actions!(\n    gpuicn,\n    [\n        /// Move to the next keyboard tab stop.\n        FocusNext,\n        /// Move to the previous keyboard tab stop.\n        FocusPrevious\n    ]\n);\n\nstruct Initialized;\nimpl Global for Initialized {}\n\n/// Installs component actions, keyboard traversal and the default theme once.\n/// Call this from application startup for both copied source and crate usage.\npub fn init(cx: &mut App) {\n    if cx.has_global::<Initialized>() {\n        return;\n    }\n    seed_component_fonts(cx);\n    gpui_kit::init(cx);\n    cx.set_global(Initialized);\n    if !cx.has_global::<UiTheme>() {\n        UiTheme::set(cx, UiTheme::neutral_light());\n    } else {\n        let theme = UiTheme::read(cx).clone();\n        sync_component_theme(cx, theme);\n    }\n    cx.bind_keys([\n        gpui_kit::KeyBinding::new(\"tab\", FocusNext, None),\n        gpui_kit::KeyBinding::new(\"shift-tab\", FocusPrevious, None),\n    ]);\n    cx.on_action(|_: &FocusNext, cx| advance_focus(false, cx));\n    cx.on_action(|_: &FocusPrevious, cx| advance_focus(true, cx));\n    // Scoped component bindings take precedence over these window-wide defaults.\n    #[cfg(target_family = \"wasm\")]\n    {\n        // Browser previews also accept macOS editing shortcuts.\n        use gpui_kit::KeyBinding;\n        use gpui_kit::base::input::{\n            Copy, Cut, MoveEnd, MoveHome, MoveToNextWord, MoveToPreviousWord, Paste, Redo,\n            SelectAll, SelectToNextWordEnd, SelectToPreviousWordStart, Undo,\n        };\n        cx.bind_keys([\n            KeyBinding::new(\"cmd-a\", SelectAll, Some(\"Input\")),\n            KeyBinding::new(\"cmd-c\", Copy, Some(\"Input\")),\n            KeyBinding::new(\"cmd-v\", Paste, Some(\"Input\")),\n            KeyBinding::new(\"cmd-x\", Cut, Some(\"Input\")),\n            KeyBinding::new(\"cmd-left\", MoveHome, Some(\"Input\")),\n            KeyBinding::new(\"cmd-right\", MoveEnd, Some(\"Input\")),\n            KeyBinding::new(\"cmd-z\", Undo, Some(\"Input\")),\n            KeyBinding::new(\"cmd-shift-z\", Redo, Some(\"Input\")),\n            KeyBinding::new(\"alt-left\", MoveToPreviousWord, Some(\"Input\")),\n            KeyBinding::new(\"alt-right\", MoveToNextWord, Some(\"Input\")),\n            KeyBinding::new(\"alt-shift-left\", SelectToPreviousWordStart, Some(\"Input\")),\n            KeyBinding::new(\"alt-shift-right\", SelectToNextWordEnd, Some(\"Input\")),\n        ]);\n    }\n}\n\nfn seed_component_fonts(cx: &mut App) {\n    use gpui_kit::component::Theme;\n\n    if cx.has_global::<Theme>() {\n        return;\n    }\n    let fonts = if cx.has_global::<UiTheme>() {\n        UiTheme::read(cx).fonts.clone()\n    } else {\n        UiTheme::neutral_light().fonts\n    };\n    let mut component = Theme::default();\n    component.font_family = fonts.body;\n    component.mono_font_family = fonts.mono;\n    cx.set_global(component);\n}\n\nfn advance_focus(reverse: bool, cx: &mut App) {\n    let Some(handle) = cx.active_window() else {\n        return;\n    };\n    // Action dispatch still holds the window borrow until this callback returns.\n    cx.defer(move |cx| {\n        let _ = handle.update(cx, |_, window, cx| {\n            if reverse {\n                window.focus_prev(cx);\n            } else {\n                window.focus_next(cx);\n            }\n        });\n    });\n}\n\n/// The active color mode.\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub enum ThemeMode {\n    /// The Neutral light palette.\n    Light,\n    /// The Neutral dark palette.\n    Dark,\n}\n\n/// Which of an application's two palettes is shown.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum Appearance {\n    /// Always the light palette.\n    Light,\n    /// Always the dark palette.\n    Dark,\n    /// The palette matching the operating system's window appearance.\n    #[default]\n    System,\n}\n\nimpl Appearance {\n    /// The mode this appearance shows under the given system appearance.\n    pub fn resolve(self, system: WindowAppearance) -> ThemeMode {\n        match self {\n            Appearance::Light => ThemeMode::Light,\n            Appearance::Dark => ThemeMode::Dark,\n            Appearance::System => match system {\n                WindowAppearance::Dark | WindowAppearance::VibrantDark => ThemeMode::Dark,\n                WindowAppearance::Light | WindowAppearance::VibrantLight => ThemeMode::Light,\n            },\n        }\n    }\n}\n\n/// The application's light and dark themes and which one it shows.\n#[derive(Clone)]\nstruct ThemePair {\n    light: UiTheme,\n    dark: UiTheme,\n    appearance: Appearance,\n}\nimpl Global for ThemePair {}\n\n/// Swaps palettes when the operating system's appearance changes while the\n/// application follows it. Call it for each window the application opens and\n/// keep the subscription; `app::run` does this for the first window.\npub fn follow_system(window: &mut Window, cx: &mut App) -> Subscription {\n    UiTheme::apply_appearance(cx);\n    window.observe_window_appearance(|_, cx| UiTheme::apply_appearance(cx))\n}\n\n/// Semantic colors consumed by installed components.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiColors {\n    /// App background.\n    pub background: Rgba,\n    /// Default foreground.\n    pub foreground: Rgba,\n    /// Card background.\n    pub card: Rgba,\n    /// Card foreground.\n    pub card_foreground: Rgba,\n    /// Popover background.\n    pub popover: Rgba,\n    /// Popover foreground.\n    pub popover_foreground: Rgba,\n    /// Primary control background.\n    pub primary: Rgba,\n    /// Primary control foreground.\n    pub primary_foreground: Rgba,\n    /// Secondary control background.\n    pub secondary: Rgba,\n    /// Secondary control foreground.\n    pub secondary_foreground: Rgba,\n    /// Muted background.\n    pub muted: Rgba,\n    /// Muted foreground.\n    pub muted_foreground: Rgba,\n    /// Accent background.\n    pub accent: Rgba,\n    /// Accent foreground.\n    pub accent_foreground: Rgba,\n    /// Destructive action color.\n    pub destructive: Rgba,\n    /// Text on a solid `destructive` surface.\n    pub destructive_foreground: Rgba,\n    /// Positive status, such as a completed upload or an online presence dot.\n    pub success: Rgba,\n    /// Text on a `success` fill.\n    pub success_foreground: Rgba,\n    /// Cautionary status, such as a permalink highlight or a pending state.\n    pub warning: Rgba,\n    /// Text on a `warning` fill.\n    pub warning_foreground: Rgba,\n    /// Informational status.\n    pub info: Rgba,\n    /// Text on an `info` fill.\n    pub info_foreground: Rgba,\n    /// Default border.\n    pub border: Rgba,\n    /// Input border.\n    pub input: Rgba,\n    /// Focus ring.\n    pub ring: Rgba,\n    /// First chart color.\n    pub chart_1: Rgba,\n    /// Second chart color.\n    pub chart_2: Rgba,\n    /// Third chart color.\n    pub chart_3: Rgba,\n    /// Fourth chart color.\n    pub chart_4: Rgba,\n    /// Fifth chart color.\n    pub chart_5: Rgba,\n    /// Sidebar background.\n    pub sidebar: Rgba,\n    /// Sidebar foreground.\n    pub sidebar_foreground: Rgba,\n    /// Sidebar primary background.\n    pub sidebar_primary: Rgba,\n    /// Sidebar primary foreground.\n    pub sidebar_primary_foreground: Rgba,\n    /// Sidebar accent background.\n    pub sidebar_accent: Rgba,\n    /// Sidebar accent foreground.\n    pub sidebar_accent_foreground: Rgba,\n    /// Sidebar border.\n    pub sidebar_border: Rgba,\n    /// Sidebar focus border color.\n    pub sidebar_ring: Rgba,\n    /// Scrim behind modal surfaces.\n    pub overlay: Rgba,\n}\n\n/// Theme font family names. Applications own font loading.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiFonts {\n    /// Default body family.\n    pub body: SharedString,\n    /// Default heading family.\n    pub heading: SharedString,\n    /// Default monospace family.\n    pub mono: SharedString,\n}\n\n/// Corner radii derived from shadcn's 10px base radius.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiRadius {\n    /// Backward-compatible alias for [`Self::lg`].\n    pub base: Pixels,\n    /// Six pixels (`radius * 0.6`).\n    pub sm: Pixels,\n    /// Eight pixels (`radius * 0.8`).\n    pub md: Pixels,\n    /// Ten pixels (the configured base radius).\n    pub lg: Pixels,\n    /// Fourteen pixels (`radius * 1.4`).\n    pub xl: Pixels,\n    /// Eighteen pixels (`radius * 1.8`).\n    pub two_xl: Pixels,\n    /// Twenty-two pixels (`radius * 2.2`).\n    pub three_xl: Pixels,\n    /// Twenty-six pixels (`radius * 2.6`).\n    pub four_xl: Pixels,\n}\n\nimpl UiRadius {\n    /// Derives the complete Nova radius scale from one nonnegative base radius.\n    pub fn new(base: Pixels) -> Self {\n        assert!(f32::from(base).is_finite() && base >= px(0.));\n        Self {\n            base,\n            sm: base * 0.6,\n            md: base * 0.8,\n            lg: base,\n            xl: base * 1.4,\n            two_xl: base * 1.8,\n            three_xl: base * 2.2,\n            four_xl: base * 2.6,\n        }\n    }\n}\n\n/// Shared UI transition durations. Honor these alongside `App::reduce_motion()`.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiMotion {\n    /// Thumb and small-state transitions (150ms by default).\n    pub fast: Duration,\n    /// Panel/layout transitions (200ms by default).\n    pub normal: Duration,\n    /// Application preference; true removes movement without removing state feedback.\n    pub reduced: bool,\n    /// Shared easing for reversible state transitions.\n    pub easing: UiEasing,\n}\nimpl Default for UiMotion {\n    fn default() -> Self {\n        Self {\n            fast: Duration::from_millis(150),\n            normal: Duration::from_millis(200),\n            reduced: false,\n            easing: UiEasing::EaseInOut,\n        }\n    }\n}\n\n/// Curves supported by the shared native transition helper.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum UiEasing {\n    /// Constant-speed motion, as used by shadcn sidebar layout transitions.\n    Linear,\n    /// Fast response followed by a smooth stop.\n    EaseOut,\n    /// Smooth acceleration and deceleration for changes within a layout.\n    #[default]\n    EaseInOut,\n    /// Quick start and a long settle (`cubic-bezier(0.22, 1, 0.36, 1)`), for\n    /// surfaces that slide in from an edge, such as drawers and sheets.\n    Emphasized,\n}\nimpl UiEasing {\n    fn sample(self, progress: f32) -> f32 {\n        match self {\n            Self::Linear => progress,\n            Self::EaseOut => gpui_kit::ease_out_quint()(progress),\n            Self::EaseInOut => gpui_kit::ease_in_out(progress),\n            Self::Emphasized => gpui_kit::base::motion::Easing::CubicBezier {\n                x1: 0.22,\n                y1: 1.,\n                x2: 0.36,\n                y2: 1.,\n            }\n            .sample(progress),\n        }\n    }\n}\n\n/// Returns a smoothly retargetable value for one stable, caller-named property.\n/// Starts at the target on mount; rapid reversals continue from the current value.\n/// Honors theme and GPUI reduced-motion settings and schedules no idle frames.\n/// Use for state changes, never for pointer-drag coordinates or keyboard navigation.\n/// Native layout transitions perform layout work; retain expensive child entities.\npub fn transition_value(\n    id: impl Into<ElementId>,\n    target: f32,\n    duration: Duration,\n    window: &mut Window,\n    cx: &mut App,\n) -> f32 {\n    let easing = UiTheme::read(cx).motion.easing;\n    transition_with(id, target, duration, easing, window, cx)\n}\n\n/// [`transition_value`] with an explicit curve, for the few surfaces whose motion\n/// differs from the theme default (drawers slide with [`UiEasing::Emphasized`]).\npub fn transition_with(\n    id: impl Into<ElementId>,\n    target: f32,\n    duration: Duration,\n    easing: UiEasing,\n    window: &mut Window,\n    cx: &mut App,\n) -> f32 {\n    assert!(target.is_finite(), \"transition targets must be finite\");\n    gpui_kit::base::motion::transition(\n        id.into(),\n        target,\n        motion_policy(duration, easing, cx),\n        window,\n        cx,\n    )\n}\n\n/// Shares the theme's disclosure and notification timing with Kit's presence lifecycle.\n/// Returns a sample whose `progress` runs 0→1 while entering and 1→0 while leaving, so\n/// a surface fades or scales with it; `should_render()` keeps the element mounted\n/// until the exit transition has finished.\n#[doc(hidden)]\npub fn presence(\n    id: impl Into<ElementId>,\n    present: bool,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::base::motion::PresenceSample {\n    let motion = UiTheme::read(cx).motion;\n    presence_with(id, present, motion.fast, motion.easing, window, cx)\n}\n\n/// [`presence`] with explicit timing and curve, for panels that take the theme's\n/// `normal` duration or a different easing.\n#[doc(hidden)]\npub fn presence_with(\n    id: impl Into<ElementId>,\n    present: bool,\n    duration: Duration,\n    easing: UiEasing,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::base::motion::PresenceSample {\n    gpui_kit::base::motion::Presence::new(id.into(), present)\n        .transition(motion_policy(duration, easing, cx))\n        .sample(window, cx)\n}\n\n/// The one place motion honors the theme and platform reduced-motion settings.\nfn motion_policy(\n    duration: Duration,\n    easing: UiEasing,\n    cx: &App,\n) -> gpui_kit::base::motion::Transition {\n    let duration = if UiTheme::read(cx).motion.reduced || cx.reduce_motion() {\n        Duration::ZERO\n    } else {\n        duration\n    };\n    gpui_kit::base::motion::Transition::new(duration).ease(move |progress| easing.sample(progress))\n}\n\n/// Shared shadcn elevation tokens. Each level is shared, so cloning a theme\n/// every render costs no allocation; `to_vec()` the level you apply.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiShadows {\n    /// Tailwind `shadow-sm`.\n    pub sm: Arc<[BoxShadow]>,\n    /// Tailwind `shadow-md`.\n    pub md: Arc<[BoxShadow]>,\n    /// Tailwind `shadow-lg`.\n    pub lg: Arc<[BoxShadow]>,\n}\n\n/// One step of the type scale: a text size and the line height that goes with it.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiTextStyle {\n    /// Font size before `text_scale`.\n    pub size: Pixels,\n    /// Line height before `text_scale`.\n    pub line_height: Pixels,\n}\n\n/// The type scale, Tailwind's by default. Components read sizes from here through\n/// `primitives::text`, so `text_scale` and a custom scale reach every label.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiTypography {\n    /// Captions and key caps (12/16).\n    pub xs: UiTextStyle,\n    /// Controls and body copy in dense desktop layouts (14/20); the Nova default.\n    pub sm: UiTextStyle,\n    /// Prose and dialog titles (16/24).\n    pub base: UiTextStyle,\n    /// Section headings (18/28).\n    pub lg: UiTextStyle,\n    /// Page headings (20/28).\n    pub xl: UiTextStyle,\n}\n\nimpl Default for UiTypography {\n    fn default() -> Self {\n        let style = |size: f32, line_height: f32| UiTextStyle {\n            size: px(size),\n            line_height: px(line_height),\n        };\n        Self {\n            xs: style(12., 16.),\n            sm: style(14., 20.),\n            base: style(16., 24.),\n            lg: style(18., 28.),\n            xl: style(20., 28.),\n        }\n    }\n}\n\n/// Window widths at which layouts change shape.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiBreakpoints {\n    /// Below this width, sidebars become sheets and drawers fill the window (768px).\n    pub mobile: Pixels,\n    /// Below this width, side drawers take three quarters of the window (640px).\n    pub compact: Pixels,\n}\n\nimpl Default for UiBreakpoints {\n    fn default() -> Self {\n        Self {\n            mobile: px(768.),\n            compact: px(640.),\n        }\n    }\n}\n\n/// Colors every form control shares, derived once from the palette and mode so\n/// no component chooses its own dark-mode alpha. These follow shadcn Nova's\n/// `dark:bg-input/30`, `dark:hover:bg-input/50` and `dark:border-input` rules.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct ControlColors {\n    /// Resting fill of inputs, checkboxes, radios and outline buttons.\n    pub background: Rgba,\n    /// Resting border of the same controls.\n    pub border: Rgba,\n    /// Hover fill of outline, ghost and toggle controls.\n    pub hover: Rgba,\n    /// Hover fill of ghost buttons and plain icon buttons.\n    pub ghost_hover: Rgba,\n    /// Fill of a pressed toggle or selected tab.\n    pub pressed: Rgba,\n    /// Unchecked switch track.\n    pub track: Rgba,\n    /// Switch thumb on the unchecked track.\n    pub thumb: Rgba,\n    /// Switch thumb on the checked (`primary`) track.\n    pub thumb_checked: Rgba,\n    /// Tinted destructive fill, for destructive buttons, badges and tags.\n    pub destructive_background: Rgba,\n    /// Hover of the tinted destructive fill.\n    pub destructive_hover: Rgba,\n    /// Hairline around tinted destructive chips.\n    pub destructive_border: Rgba,\n    /// Focus ring color at Nova's 50% opacity.\n    pub ring: Rgba,\n    /// Focus ring of an invalid control.\n    pub invalid_ring: Rgba,\n    /// Hairline around popovers and dialogs (`ring-foreground/10`).\n    pub surface_border: Rgba,\n}\n\n/// Base spacing unit from the pinned Nova style.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiSpacing {\n    /// Four pixels.\n    pub unit: Pixels,\n}\n\n/// The application-owned theme stored in GPUI global state.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiTheme {\n    /// Active color mode.\n    pub mode: ThemeMode,\n    /// Active semantic colors.\n    pub colors: UiColors,\n    /// Font family names.\n    pub fonts: UiFonts,\n    /// Base corner radius.\n    pub radius: UiRadius,\n    /// Base spacing unit.\n    pub spacing: UiSpacing,\n    /// Shared elevation tokens.\n    pub shadows: UiShadows,\n    /// Shared motion durations and reduced-motion preference.\n    pub motion: UiMotion,\n    /// Typography scale, independent of control density (1 by default).\n    pub text_scale: f32,\n    /// Text sizes and line heights, multiplied by `text_scale` when applied.\n    pub typography: UiTypography,\n    /// Window widths at which layouts change shape.\n    pub breakpoints: UiBreakpoints,\n}\n\nimpl Global for UiTheme {}\n\nimpl Default for UiTheme {\n    fn default() -> Self {\n        Self::neutral_light()\n    }\n}\n\nimpl UiTheme {\n    /// Creates the pinned Neutral light theme.\n    pub fn neutral_light() -> Self {\n        Self::new(ThemeMode::Light, neutral_light_colors())\n    }\n\n    /// Creates the pinned Neutral dark theme.\n    pub fn neutral_dark() -> Self {\n        Self::new(ThemeMode::Dark, neutral_dark_colors())\n    }\n\n    /// Installs light and dark themes together and shows the one the current\n    /// [`Appearance`] selects (System until [`UiTheme::set_appearance`] says\n    /// otherwise). [`follow_system`] keeps the pair in step with the OS.\n    pub fn set_pair(cx: &mut App, light: Self, dark: Self) {\n        let appearance = cx\n            .try_global::<ThemePair>()\n            .map_or(Appearance::System, |pair| pair.appearance);\n        cx.set_global(ThemePair {\n            light,\n            dark,\n            appearance,\n        });\n        Self::apply_appearance(cx);\n    }\n\n    /// Chooses which of the installed pair to show. Without a pair, the\n    /// pinned Neutral palettes stand in.\n    pub fn set_appearance(cx: &mut App, appearance: Appearance) {\n        match cx.try_global::<ThemePair>() {\n            Some(_) => cx.global_mut::<ThemePair>().appearance = appearance,\n            None => cx.set_global(ThemePair {\n                light: Self::neutral_light(),\n                dark: Self::neutral_dark(),\n                appearance,\n            }),\n        }\n        Self::apply_appearance(cx);\n    }\n\n    /// The appearance the application asked for, System when none was set.\n    pub fn appearance(cx: &App) -> Appearance {\n        cx.try_global::<ThemePair>()\n            .map_or(Appearance::System, |pair| pair.appearance)\n    }\n\n    /// Installs the pair member the appearance selects, if a pair is installed.\n    fn apply_appearance(cx: &mut App) {\n        let Some(pair) = cx.try_global::<ThemePair>().cloned() else {\n            return;\n        };\n        let theme = match pair.appearance.resolve(cx.window_appearance()) {\n            ThemeMode::Light => pair.light,\n            ThemeMode::Dark => pair.dark,\n        };\n        let current = cx.try_global::<Self>();\n        if current\n            .is_some_and(|current| current.mode == theme.mode && current.colors == theme.colors)\n        {\n            return;\n        }\n        Self::set(cx, theme);\n    }\n\n    /// Installs a theme into the application.\n    pub fn set(cx: &mut App, theme: Self) {\n        assert!(f32::from(theme.spacing.unit).is_finite() && theme.spacing.unit > px(0.));\n        assert!(theme.text_scale.is_finite() && theme.text_scale > 0.);\n        sync_component_theme(cx, theme.clone());\n        cx.set_global(theme);\n        cx.refresh_windows();\n    }\n\n    /// Reads the installed theme.\n    ///\n    /// Panics when the application has not installed a theme.\n    #[track_caller]\n    pub fn read(cx: &App) -> &Self {\n        cx.try_global::<Self>().expect(\n            \"gpuicn theme is not installed: call `ui::theme::init(cx)` once at application startup\",\n        )\n    }\n\n    /// Builds a theme from shadcn theme CSS, such as the output of the shadcn\n    /// theme picker or tweakcn.\n    ///\n    /// Light reads `:root`; dark reads `:root` and then `.dark`, like the\n    /// browser cascade. Recognized `--background`, `--primary`, `--sidebar-*`,\n    /// `--chart-*` and `--radius` declarations replace the Neutral defaults;\n    /// other declarations are ignored. Colors may use `oklch()`, `hsl()`,\n    /// bare HSL triplets, `rgb()` or hex. Build both modes and call\n    /// [`UiTheme::set`] to switch; [`UiTheme::switch`] restores Neutral colors.\n    pub fn from_shadcn_css(css: &str, mode: ThemeMode) -> Result<Self, ThemeCssError> {\n        let blocks = css_blocks(css);\n        let mut theme = match mode {\n            ThemeMode::Light => Self::neutral_light(),\n            ThemeMode::Dark => Self::neutral_dark(),\n        };\n        let mut selectors = vec![\":root\"];\n        if mode == ThemeMode::Dark {\n            selectors.push(\".dark\");\n        }\n        for selector in selectors {\n            let declarations = blocks\n                .iter()\n                .filter(|(name, _)| name.split(',').any(|part| part.trim() == selector))\n                .flat_map(|(_, body)| body.split(';'))\n                .collect::<Vec<_>>();\n            if declarations.is_empty() {\n                return Err(ThemeCssError(format!(\"no `{selector}` block found\")));\n            }\n            for declaration in declarations {\n                let Some((name, value)) = declaration.split_once(':') else {\n                    continue;\n                };\n                let (name, value) = (name.trim(), value.trim());\n                let Some(name) = name.strip_prefix(\"--\") else {\n                    continue;\n                };\n                if name == \"radius\" {\n                    theme.radius = UiRadius::new(css_length(value).ok_or_else(|| {\n                        ThemeCssError(format!(\"unsupported length `--radius: {value}`\"))\n                    })?);\n                } else if let Some(slot) = theme.colors.slot(name) {\n                    *slot = css_color(value).ok_or_else(|| {\n                        ThemeCssError(format!(\"unsupported color `--{name}: {value}`\"))\n                    })?;\n                }\n            }\n        }\n        Ok(theme)\n    }\n\n    /// Selects the pinned palette, preserving non-color tokens or initializing defaults.\n    pub fn switch(cx: &mut App, mode: ThemeMode) {\n        let mut theme = cx\n            .try_global::<Self>()\n            .cloned()\n            .unwrap_or_else(Self::neutral_light);\n        theme.mode = mode;\n        theme.colors = match mode {\n            ThemeMode::Light => neutral_light_colors(),\n            ThemeMode::Dark => neutral_dark_colors(),\n        };\n        Self::set(cx, theme);\n    }\n\n    /// Returns a multiple of the configured spacing unit (2 means 8px by default).\n    pub fn space(&self, units: f32) -> Pixels {\n        self.spacing.unit * units\n    }\n\n    /// Scales typography independently of spacing; sizes use Nova's default pixels.\n    pub fn text(&self, size: f32) -> Pixels {\n        px(size * self.text_scale)\n    }\n\n    fn new(mode: ThemeMode, colors: UiColors) -> Self {\n        Self {\n            mode,\n            colors,\n            fonts: UiFonts {\n                body: \"Geist\".into(),\n                heading: \"Geist\".into(),\n                mono: \"Geist Mono\".into(),\n            },\n            radius: UiRadius::new(px(10.)),\n            motion: UiMotion::default(),\n            text_scale: 1.,\n            typography: UiTypography::default(),\n            breakpoints: UiBreakpoints::default(),\n            spacing: UiSpacing { unit: px(4.) },\n            shadows: UiShadows {\n                sm: Arc::from([\n                    BoxShadow::new(px(0.), px(1.), black().alpha(0.10)).blur_radius(px(3.)),\n                    BoxShadow::new(px(0.), px(1.), black().alpha(0.10))\n                        .blur_radius(px(2.))\n                        .spread_radius(px(-1.)),\n                ]),\n                md: Arc::from([\n                    BoxShadow::new(px(0.), px(4.), black().alpha(0.10))\n                        .blur_radius(px(6.))\n                        .spread_radius(px(-1.)),\n                    BoxShadow::new(px(0.), px(2.), black().alpha(0.10))\n                        .blur_radius(px(4.))\n                        .spread_radius(px(-2.)),\n                ]),\n                lg: Arc::from([\n                    BoxShadow::new(px(0.), px(10.), black().alpha(0.10))\n                        .blur_radius(px(15.))\n                        .spread_radius(px(-3.)),\n                    BoxShadow::new(px(0.), px(4.), black().alpha(0.10))\n                        .blur_radius(px(6.))\n                        .spread_radius(px(-4.)),\n                ]),\n            },\n        }\n    }\n\n    /// The shared form-control colors for the active mode. See [`ControlColors`].\n    pub fn control(&self) -> ControlColors {\n        let colors = self.colors;\n        let dark = self.mode == ThemeMode::Dark;\n        ControlColors {\n            background: if dark {\n                colors.input.opacity(0.30)\n            } else {\n                colors.background.opacity(0.)\n            },\n            border: colors.input,\n            hover: if dark {\n                colors.input.opacity(0.50)\n            } else {\n                colors.muted\n            },\n            ghost_hover: if dark {\n                colors.muted.opacity(0.50)\n            } else {\n                colors.muted\n            },\n            pressed: colors.muted,\n            track: if dark {\n                colors.input.opacity(0.80)\n            } else {\n                colors.input\n            },\n            thumb: if dark {\n                colors.foreground\n            } else {\n                colors.background\n            },\n            thumb_checked: if dark {\n                colors.primary_foreground\n            } else {\n                colors.background\n            },\n            destructive_background: colors.destructive.opacity(if dark { 0.20 } else { 0.10 }),\n            destructive_hover: colors.destructive.opacity(if dark { 0.30 } else { 0.20 }),\n            destructive_border: colors.destructive.opacity(0.20),\n            ring: colors.ring.opacity(0.50),\n            invalid_ring: colors.destructive.opacity(0.20),\n            surface_border: colors.foreground.opacity(0.10),\n        }\n    }\n\n    /// A step of the type scale multiplied by `text_scale`.\n    pub fn text_style(&self, style: UiTextStyle) -> UiTextStyle {\n        UiTextStyle {\n            size: style.size * self.text_scale,\n            line_height: style.line_height * self.text_scale,\n        }\n    }\n}\n\nfn sync_component_theme(cx: &mut App, theme: UiTheme) {\n    use gpui_kit::component::{Theme, ThemeTokens};\n\n    if !cx.has_global::<Theme>() {\n        return;\n    }\n\n    let colors = theme.colors;\n    let component = Theme::global_mut(cx);\n    component.mode = match theme.mode {\n        ThemeMode::Light => gpui_kit::component::ThemeMode::Light,\n        ThemeMode::Dark => gpui_kit::component::ThemeMode::Dark,\n    };\n    // Start from Kit's palette for this mode, so colors gpuicn doesn't set\n    // below, such as scrollbar tracks, never keep the other mode's values.\n    component.colors = *match theme.mode {\n        ThemeMode::Light => gpui_kit::component::ThemeColor::light(),\n        ThemeMode::Dark => gpui_kit::component::ThemeColor::dark(),\n    };\n    component.font_family = theme.fonts.body;\n    component.mono_font_family = theme.fonts.mono;\n    component.radius = theme.radius.md;\n    component.radius_lg = theme.radius.lg;\n    component.background = colors.background.into();\n    component.foreground = colors.foreground.into();\n    component.popover = colors.popover.into();\n    component.popover_foreground = colors.popover_foreground.into();\n    component.primary = colors.primary.into();\n    component.primary_foreground = colors.primary_foreground.into();\n    component.secondary = colors.secondary.into();\n    component.secondary_foreground = colors.secondary_foreground.into();\n    component.muted = colors.muted.into();\n    component.muted_foreground = colors.muted_foreground.into();\n    component.accent = colors.accent.into();\n    component.accent_foreground = colors.accent_foreground.into();\n    component.primary_hover = colors.primary.opacity(0.9).into();\n    component.primary_active = colors.primary.opacity(0.8).into();\n    component.secondary_hover = colors.secondary.opacity(0.8).into();\n    component.secondary_active = colors.secondary.opacity(0.7).into();\n    component.border = colors.border.into();\n    component.input = colors.input.into();\n    component.ring = colors.ring.into();\n    component.overlay = colors.overlay.into();\n    component.caret = colors.foreground.into();\n    component.window_border = colors.border.into();\n    component.danger = colors.destructive.into();\n    component.danger_foreground = colors.destructive_foreground.into();\n    component.danger_hover = colors.destructive.opacity(0.9).into();\n    component.danger_active = colors.destructive.opacity(0.8).into();\n    component.success = colors.success.into();\n    component.success_foreground = colors.success_foreground.into();\n    component.success_hover = colors.success.opacity(0.9).into();\n    component.success_active = colors.success.opacity(0.8).into();\n    component.warning = colors.warning.into();\n    component.warning_foreground = colors.warning_foreground.into();\n    component.warning_hover = colors.warning.opacity(0.9).into();\n    component.warning_active = colors.warning.opacity(0.8).into();\n    component.info = colors.info.into();\n    component.info_foreground = colors.info_foreground.into();\n    component.info_hover = colors.info.opacity(0.9).into();\n    component.info_active = colors.info.opacity(0.8).into();\n    component.chart_1 = colors.chart_1.into();\n    component.chart_2 = colors.chart_2.into();\n    component.chart_3 = colors.chart_3.into();\n    component.chart_4 = colors.chart_4.into();\n    component.chart_5 = colors.chart_5.into();\n    component.chart_bullish = colors.success.into();\n    component.chart_bearish = colors.destructive.into();\n    component.chart_grid = colors.border.opacity(0.6).into();\n    // Buttons: Kit's plain button is shadcn `outline`; the variants follow their token.\n    component.button = colors.background.into();\n    component.button_foreground = colors.foreground.into();\n    component.button_hover = colors.accent.into();\n    component.button_active = colors.muted.into();\n    component.button_primary = component.primary;\n    component.button_primary_foreground = component.primary_foreground;\n    component.button_primary_hover = component.primary_hover;\n    component.button_primary_active = component.primary_active;\n    component.button_secondary = component.secondary;\n    component.button_secondary_foreground = component.secondary_foreground;\n    component.button_secondary_hover = component.secondary_hover;\n    component.button_secondary_active = component.secondary_active;\n    component.button_danger = component.danger;\n    component.button_danger_foreground = component.danger_foreground;\n    component.button_danger_hover = component.danger_hover;\n    component.button_danger_active = component.danger_active;\n    component.button_success = component.success;\n    component.button_success_foreground = component.success_foreground;\n    component.button_success_hover = component.success_hover;\n    component.button_success_active = component.success_active;\n    component.button_warning = component.warning;\n    component.button_warning_foreground = component.warning_foreground;\n    component.button_warning_hover = component.warning_hover;\n    component.button_warning_active = component.warning_active;\n    component.button_info = component.info;\n    component.button_info_foreground = component.info_foreground;\n    component.button_info_hover = component.info_hover;\n    component.button_info_active = component.info_active;\n    // Nova scrollbars float over content with no track.\n    component.scrollbar = gpui_kit::transparent_black();\n    component.scrollbar_thumb = colors.border.into();\n    component.scrollbar_thumb_hover = colors.muted_foreground.opacity(0.72).into();\n    component.sidebar = colors.sidebar.into();\n    component.sidebar_foreground = colors.sidebar_foreground.into();\n    component.sidebar_border = colors.sidebar_border.into();\n    component.sidebar_accent = colors.sidebar_accent.into();\n    component.sidebar_accent_foreground = colors.sidebar_accent_foreground.into();\n    component.sidebar_primary = colors.sidebar_primary.into();\n    component.sidebar_primary_foreground = colors.sidebar_primary_foreground.into();\n    component.title_bar = colors.sidebar.into();\n    component.title_bar_border = colors.border.into();\n    component.status_bar = colors.sidebar.into();\n    component.status_bar_border = colors.border.into();\n    // Rich text: links, selection, table headings and code highlighting.\n    component.link = colors.primary.into();\n    component.link_hover = colors.primary.opacity(0.9).into();\n    component.link_active = colors.primary.opacity(0.8).into();\n    component.selection = colors.ring.opacity(0.3).into();\n    component.description_list_label = colors.muted.into();\n    component.description_list_label_foreground = colors.muted_foreground.into();\n    component.group_box = colors.card.into();\n    component.group_box_foreground = colors.card_foreground.into();\n    component.accordion = colors.card.into();\n    // Lists and tables share the `muted` zebra and the `accent` hover of menus.\n    component.colors.list = colors.background.into();\n    component.list_even = colors.muted.opacity(0.5).into();\n    component.list_head = colors.muted.into();\n    component.list_hover = colors.accent.into();\n    component.list_active = colors.accent.into();\n    component.list_active_border = colors.ring.into();\n    component.table = colors.background.into();\n    component.table_even = colors.muted.opacity(0.5).into();\n    component.table_hover = colors.muted.opacity(0.5).into();\n    component.table_active = colors.accent.into();\n    component.table_active_border = colors.ring.into();\n    component.table_row_border = colors.border.into();\n    component.table_head = colors.muted.into();\n    component.table_head_foreground = colors.foreground.into();\n    component.table_foot = colors.muted.opacity(0.5).into();\n    component.table_foot_foreground = colors.foreground.into();\n    // Controls: shadcn tracks are `input`, thumbs and active tabs are `background`.\n    component.switch = colors.input.into();\n    component.switch_thumb = colors.background.into();\n    component.slider_bar = colors.primary.into();\n    component.slider_thumb = colors.background.into();\n    component.progress_bar = colors.primary.into();\n    component.skeleton = colors.accent.into();\n    component.tab_bar = colors.muted.into();\n    component.tab_bar_segmented = colors.muted.into();\n    component.tab_foreground = colors.muted_foreground.into();\n    component.tab_active = colors.background.into();\n    component.tab_active_foreground = colors.foreground.into();\n    component.drag_border = colors.ring.into();\n    component.drop_target = colors.ring.opacity(0.15).into();\n    component.highlight_theme = match theme.mode {\n        ThemeMode::Light => gpui_kit::component::highlighter::HighlightTheme::default_light(),\n        ThemeMode::Dark => gpui_kit::component::highlighter::HighlightTheme::default_dark(),\n    };\n    component.tokens = ThemeTokens::from(&component.colors);\n\n    Theme::sync_base(cx);\n}\n\n/// Why [`UiTheme::from_shadcn_css`] could not read a theme.\n#[derive(Clone, Debug, PartialEq)]\npub struct ThemeCssError(pub String);\n\nimpl std::fmt::Display for ThemeCssError {\n    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(formatter, \"shadcn theme CSS: {}\", self.0)\n    }\n}\n\nimpl std::error::Error for ThemeCssError {}\n\nimpl UiColors {\n    #[doc(hidden)]\n    pub fn slot(&mut self, variable: &str) -> Option<&mut Rgba> {\n        Some(match variable {\n            \"background\" => &mut self.background,\n            \"foreground\" => &mut self.foreground,\n            \"card\" => &mut self.card,\n            \"card-foreground\" => &mut self.card_foreground,\n            \"popover\" => &mut self.popover,\n            \"popover-foreground\" => &mut self.popover_foreground,\n            \"primary\" => &mut self.primary,\n            \"primary-foreground\" => &mut self.primary_foreground,\n            \"secondary\" => &mut self.secondary,\n            \"secondary-foreground\" => &mut self.secondary_foreground,\n            \"muted\" => &mut self.muted,\n            \"muted-foreground\" => &mut self.muted_foreground,\n            \"accent\" => &mut self.accent,\n            \"accent-foreground\" => &mut self.accent_foreground,\n            \"destructive\" => &mut self.destructive,\n            \"destructive-foreground\" => &mut self.destructive_foreground,\n            \"success\" => &mut self.success,\n            \"success-foreground\" => &mut self.success_foreground,\n            \"warning\" => &mut self.warning,\n            \"warning-foreground\" => &mut self.warning_foreground,\n            \"info\" => &mut self.info,\n            \"info-foreground\" => &mut self.info_foreground,\n            \"border\" => &mut self.border,\n            \"input\" => &mut self.input,\n            \"ring\" => &mut self.ring,\n            \"chart-1\" => &mut self.chart_1,\n            \"chart-2\" => &mut self.chart_2,\n            \"chart-3\" => &mut self.chart_3,\n            \"chart-4\" => &mut self.chart_4,\n            \"chart-5\" => &mut self.chart_5,\n            \"sidebar\" | \"sidebar-background\" => &mut self.sidebar,\n            \"sidebar-foreground\" => &mut self.sidebar_foreground,\n            \"sidebar-primary\" => &mut self.sidebar_primary,\n            \"sidebar-primary-foreground\" => &mut self.sidebar_primary_foreground,\n            \"sidebar-accent\" => &mut self.sidebar_accent,\n            \"sidebar-accent-foreground\" => &mut self.sidebar_accent_foreground,\n            \"sidebar-border\" => &mut self.sidebar_border,\n            \"sidebar-ring\" => &mut self.sidebar_ring,\n            _ => return None,\n        })\n    }\n}\n\n/// Innermost `selector { body }` pairs with comments removed.\nfn css_blocks(css: &str) -> Vec<(String, String)> {\n    let mut text = String::with_capacity(css.len());\n    let mut rest = css;\n    while let Some(start) = rest.find(\"/*\") {\n        text.push_str(&rest[..start]);\n        rest = rest[start + 2..]\n            .split_once(\"*/\")\n            .map_or(\"\", |(_, after)| after);\n    }\n    text.push_str(rest);\n    let mut blocks = Vec::new();\n    let mut selector_start = 0;\n    let mut open = None;\n    for (index, character) in text.char_indices() {\n        match character {\n            '{' => {\n                open = Some((text[selector_start..index].trim().to_owned(), index + 1));\n                selector_start = index + 1;\n            }\n            '}' => {\n                if let Some((selector, body)) = open.take() {\n                    blocks.push((selector, text[body..index].to_owned()));\n                }\n                selector_start = index + 1;\n            }\n            ';' if open.is_none() => selector_start = index + 1,\n            _ => {}\n        }\n    }\n    blocks\n}\n\nfn css_length(value: &str) -> Option<Pixels> {\n    let value = value.trim();\n    let pixels = if let Some(rem) = value.strip_suffix(\"rem\") {\n        rem.trim().parse::<f32>().ok()? * 16.\n    } else if let Some(pixels) = value.strip_suffix(\"px\") {\n        pixels.trim().parse::<f32>().ok()?\n    } else {\n        value.parse::<f32>().ok().filter(|value| *value == 0.)?\n    };\n    (pixels.is_finite() && pixels >= 0.).then(|| px(pixels))\n}\n\n/// A CSS number or percentage; `percent` is the value of `100%`.\nfn css_number(value: &str, percent: f64) -> Option<f64> {\n    let value = value.trim();\n    let number = match value.strip_suffix('%') {\n        Some(value) => value.trim().parse::<f64>().ok()? / 100. * percent,\n        None => value\n            .strip_suffix(\"deg\")\n            .unwrap_or(value)\n            .parse::<f64>()\n            .ok()?,\n    };\n    number.is_finite().then_some(number)\n}\n\nfn css_color(value: &str) -> Option<Rgba> {\n    let value = value.trim().trim_end_matches(\"!important\").trim();\n    if let Some(hex) = value.strip_prefix('#') {\n        return css_hex(hex);\n    }\n    let (function, arguments) = match value.split_once('(') {\n        Some((function, arguments)) => (\n            function.trim().to_ascii_lowercase(),\n            arguments.strip_suffix(')')?,\n        ),\n        // shadcn v3 themes store bare HSL triplets such as `0 0% 100%`.\n        None => (\"hsl\".to_owned(), value),\n    };\n    let (channels, alpha) = match arguments.split_once('/') {\n        Some((channels, alpha)) => (channels, css_number(alpha, 1.)?),\n        None => (arguments, 1.),\n    };\n    let channels: Vec<_> = channels\n        .split(|character: char| character == ',' || character.is_whitespace())\n        .filter(|part| !part.is_empty())\n        .collect();\n    let (channels, alpha) = match channels.len() {\n        3 => (channels, alpha),\n        4 => (channels[..3].to_vec(), css_number(channels[3], 1.)?),\n        _ => return None,\n    };\n    let color = match function.as_str() {\n        \"oklch\" => oklch(\n            css_number(channels[0], 1.)?,\n            css_number(channels[1], 0.4)?,\n            css_number(channels[2], 1.)?,\n        ),\n        \"hsl\" | \"hsla\" => hsl(\n            css_number(channels[0], 1.)?,\n            css_number(channels[1], 1.)?,\n            css_number(channels[2], 1.)?,\n        ),\n        \"rgb\" | \"rgba\" => [\n            css_number(channels[0], 255.)? / 255.,\n            css_number(channels[1], 255.)? / 255.,\n            css_number(channels[2], 255.)? / 255.,\n        ],\n        _ => return None,\n    };\n    Some(Rgba {\n        r: color[0].clamp(0., 1.) as f32,\n        g: color[1].clamp(0., 1.) as f32,\n        b: color[2].clamp(0., 1.) as f32,\n        a: alpha.clamp(0., 1.) as f32,\n    })\n}\n\nfn css_hex(hex: &str) -> Option<Rgba> {\n    if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {\n        return None;\n    }\n    let digits: Vec<u8> = match hex.len() {\n        3 | 4 => hex\n            .chars()\n            .map(|digit| u8::from_str_radix(&format!(\"{digit}{digit}\"), 16).ok())\n            .collect::<Option<_>>()?,\n        6 | 8 => (0..hex.len())\n            .step_by(2)\n            .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).ok())\n            .collect::<Option<_>>()?,\n        _ => return None,\n    };\n    Some(Rgba {\n        r: digits[0] as f32 / 255.,\n        g: digits[1] as f32 / 255.,\n        b: digits[2] as f32 / 255.,\n        a: digits.get(3).map_or(1., |alpha| *alpha as f32 / 255.),\n    })\n}\n\n/// OKLCH to gamma-encoded sRGB, matching the pinned Neutral conversion when chroma is 0.\nfn oklch(lightness: f64, chroma: f64, hue: f64) -> [f64; 3] {\n    let (a, b) = (\n        chroma * hue.to_radians().cos(),\n        chroma * hue.to_radians().sin(),\n    );\n    let l = (lightness + 0.396_337_777_4 * a + 0.215_803_757_3 * b).powi(3);\n    let m = (lightness - 0.105_561_345_8 * a - 0.063_854_172_8 * b).powi(3);\n    let s = (lightness - 0.089_484_177_5 * a - 1.291_485_548 * b).powi(3);\n    [\n        4.076_741_662_1 * l - 3.307_711_591_3 * m + 0.230_969_929_2 * s,\n        -1.268_438_004_6 * l + 2.609_757_401_1 * m - 0.341_319_396_5 * s,\n        -0.004_196_086_3 * l - 0.703_418_614_7 * m + 1.707_614_701 * s,\n    ]\n    .map(srgb_channel)\n}\n\nfn hsl(hue: f64, saturation: f64, lightness: f64) -> [f64; 3] {\n    let hue = hue.rem_euclid(360.) / 60.;\n    let chroma = (1. - (2. * lightness - 1.).abs()) * saturation;\n    let x = chroma * (1. - (hue % 2. - 1.).abs());\n    let (r, g, b) = match hue as u32 {\n        0 => (chroma, x, 0.),\n        1 => (x, chroma, 0.),\n        2 => (0., chroma, x),\n        3 => (0., x, chroma),\n        4 => (x, 0., chroma),\n        _ => (chroma, 0., x),\n    };\n    let base = lightness - chroma / 2.;\n    [r + base, g + base, b + base]\n}\n\nfn neutral_light_colors() -> UiColors {\n    UiColors {\n        background: neutral(1.0),\n        foreground: neutral(0.145),\n        card: neutral(1.0),\n        card_foreground: neutral(0.145),\n        popover: neutral(1.0),\n        popover_foreground: neutral(0.145),\n        primary: neutral(0.205),\n        primary_foreground: neutral(0.985),\n        secondary: neutral(0.97),\n        secondary_foreground: neutral(0.205),\n        muted: neutral(0.97),\n        muted_foreground: neutral(0.556),\n        accent: neutral(0.97),\n        accent_foreground: neutral(0.205),\n        destructive: srgb(0xdc, 0x26, 0x26),\n        destructive_foreground: neutral(0.985),\n        success: srgb(0x16, 0xa3, 0x4a),\n        success_foreground: neutral(0.985),\n        warning: srgb(0xd9, 0x77, 0x06),\n        warning_foreground: neutral(0.985),\n        info: srgb(0x25, 0x63, 0xeb),\n        info_foreground: neutral(0.985),\n        border: neutral(0.922),\n        input: neutral(0.922),\n        ring: neutral(0.708),\n        chart_1: neutral(0.87),\n        chart_2: neutral(0.556),\n        chart_3: neutral(0.439),\n        chart_4: neutral(0.371),\n        chart_5: neutral(0.269),\n        sidebar: neutral(0.985),\n        sidebar_foreground: neutral(0.145),\n        sidebar_primary: neutral(0.205),\n        sidebar_primary_foreground: neutral(0.985),\n        sidebar_accent: neutral(0.97),\n        sidebar_accent_foreground: neutral(0.205),\n        sidebar_border: neutral(0.922),\n        sidebar_ring: neutral(0.708),\n        overlay: black().alpha(0.10).into(),\n    }\n}\n\nfn neutral_dark_colors() -> UiColors {\n    UiColors {\n        background: neutral(0.145),\n        foreground: neutral(0.985),\n        card: neutral(0.205),\n        card_foreground: neutral(0.985),\n        popover: neutral(0.205),\n        popover_foreground: neutral(0.985),\n        primary: neutral(0.922),\n        primary_foreground: neutral(0.205),\n        secondary: neutral(0.269),\n        secondary_foreground: neutral(0.985),\n        muted: neutral(0.269),\n        muted_foreground: neutral(0.708),\n        accent: neutral(0.269),\n        accent_foreground: neutral(0.985),\n        destructive: srgb(0xff, 0x64, 0x67),\n        destructive_foreground: neutral(0.985),\n        success: srgb(0x4a, 0xde, 0x80),\n        success_foreground: neutral(0.145),\n        warning: srgb(0xfb, 0xbf, 0x24),\n        warning_foreground: neutral(0.145),\n        info: srgb(0x60, 0xa5, 0xfa),\n        info_foreground: neutral(0.145),\n        border: neutral_alpha(1.0, 0.10),\n        input: neutral_alpha(1.0, 0.15),\n        ring: neutral(0.556),\n        chart_1: neutral(0.87),\n        chart_2: neutral(0.556),\n        chart_3: neutral(0.439),\n        chart_4: neutral(0.371),\n        chart_5: neutral(0.269),\n        sidebar: neutral(0.205),\n        sidebar_foreground: neutral(0.985),\n        sidebar_primary: srgb(0x14, 0x47, 0xe6),\n        sidebar_primary_foreground: neutral(0.985),\n        sidebar_accent: neutral(0.269),\n        sidebar_accent_foreground: neutral(0.985),\n        sidebar_border: neutral_alpha(1.0, 0.10),\n        sidebar_ring: neutral(0.556),\n        // shadcn pairs black/10 with a backdrop blur GPUI lacks; without it a\n        // 10% scrim disappears on a near-black page.\n        overlay: black().alpha(0.50).into(),\n    }\n}\n\n#[doc(hidden)]\npub fn neutral(l: f64) -> Rgba {\n    neutral_alpha(l, 1.0)\n}\n\nfn neutral_alpha(l: f64, alpha: f64) -> Rgba {\n    let channel = srgb_channel(l * l * l) as f32;\n\n    Rgba {\n        r: channel,\n        g: channel,\n        b: channel,\n        a: alpha.clamp(0.0, 1.0) as f32,\n    }\n}\n\nconst fn srgb(red: u8, green: u8, blue: u8) -> Rgba {\n    Rgba {\n        r: red as f32 / 255.0,\n        g: green as f32 / 255.0,\n        b: blue as f32 / 255.0,\n        a: 1.0,\n    }\n}\n\nfn srgb_channel(channel: f64) -> f64 {\n    if channel >= 0.003_130_8 {\n        (1.055 * channel.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)\n    } else {\n        (12.92 * channel).clamp(0.0, 1.0)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn converts_pinned_neutral_tokens() {\n        assert_eq!(\n            neutral(1.0),\n            Rgba {\n                r: 1.0,\n                g: 1.0,\n                b: 1.0,\n                a: 1.0,\n            }\n        );\n        assert_eq!(\n            neutral(0.0),\n            Rgba {\n                r: 0.0,\n                g: 0.0,\n                b: 0.0,\n                a: 1.0,\n            }\n        );\n\n        assert_eq!(neutral_light_colors().destructive, srgb(0xdc, 0x26, 0x26));\n        assert_eq!(neutral_dark_colors().destructive, srgb(0xff, 0x64, 0x67));\n        assert_eq!(\n            neutral_dark_colors().sidebar_primary,\n            srgb(0x14, 0x47, 0xe6)\n        );\n    }\n\n    fn close(left: Rgba, right: Rgba) -> bool {\n        [\n            (left.r, right.r),\n            (left.g, right.g),\n            (left.b, right.b),\n            (left.a, right.a),\n        ]\n        .iter()\n        .all(|(left, right)| (left - right).abs() < 1.5 / 255.)\n    }\n\n    #[test]\n    fn imports_shadcn_theme_css() {\n        let css = r#\"\n            @layer base {\n              :root {\n                --radius: 0.5rem; /* 8px */\n                --background: oklch(1 0 0);\n                --foreground: oklch(0.145 0 0);\n                --primary: #2563eb;\n                --ring: rgb(37 99 235 / 50%);\n                --sidebar-background: 0 0% 98%;\n                --font-sans: Inter, sans-serif;\n              }\n              .dark {\n                --background: oklch(0.145 0 0);\n                --border: oklch(1 0 0 / 10%);\n                --sidebar-primary: oklch(0.488 0.243 264.376);\n                --chart-1: hsl(220 70% 50%);\n              }\n            }\n        \"#;\n        let light = UiTheme::from_shadcn_css(css, ThemeMode::Light).unwrap();\n        assert_eq!(light.mode, ThemeMode::Light);\n        assert_eq!(light.radius.lg, px(8.));\n        assert!(close(light.colors.background, neutral(1.0)));\n        assert!(close(light.colors.foreground, neutral(0.145)));\n        assert_eq!(light.colors.primary, srgb(0x25, 0x63, 0xeb));\n        assert!(close(\n            light.colors.ring,\n            Rgba {\n                a: 0.5,\n                ..srgb(37, 99, 235)\n            }\n        ));\n        assert!(close(light.colors.sidebar, neutral(0.985)));\n        // Variables the CSS leaves out keep the Neutral defaults.\n        assert_eq!(light.colors.muted, neutral_light_colors().muted);\n\n        let dark = UiTheme::from_shadcn_css(css, ThemeMode::Dark).unwrap();\n        assert_eq!(dark.mode, ThemeMode::Dark);\n        assert!(close(dark.colors.background, neutral(0.145)));\n        // `.dark` inherits `:root` values it does not override.\n        assert_eq!(dark.colors.primary, srgb(0x25, 0x63, 0xeb));\n        assert!(close(dark.colors.border, neutral_alpha(1.0, 0.10)));\n        assert!(close(dark.colors.sidebar_primary, srgb(0x14, 0x47, 0xe6)));\n        assert!(close(dark.colors.chart_1, srgb(0x26, 0x63, 0xd9)));\n\n        assert!(UiTheme::from_shadcn_css(\":root { --primary: blue; }\", ThemeMode::Light).is_err());\n        assert!(UiTheme::from_shadcn_css(\":root { --radius: 1em; }\", ThemeMode::Light).is_err());\n        assert!(UiTheme::from_shadcn_css(\":root { }\", ThemeMode::Dark).is_err());\n        assert_eq!(\n            css_hex(\"fff8\"),\n            Some(Rgba {\n                r: 1.,\n                g: 1.,\n                b: 1.,\n                a: 0x88 as f32 / 255.\n            })\n        );\n    }\n\n    #[test]\n    fn preserves_pinned_alpha_tokens() {\n        let dark = neutral_dark_colors();\n        assert_eq!(dark.border.a, 0.10);\n        assert_eq!(dark.input.a, 0.15);\n        assert_eq!(dark.sidebar_border.a, 0.10);\n        assert!((dark.input.opacity(0.30).a - 0.045).abs() < f32::EPSILON);\n    }\n\n    #[test]\n    fn derives_the_shadcn_radius_and_shadow_scales() {\n        let theme = UiTheme::neutral_light();\n        assert_eq!(theme.radius.sm, px(6.));\n        assert_eq!(theme.radius.md, px(8.));\n        assert_eq!(theme.radius.lg, px(10.));\n        assert_eq!(theme.radius.xl, px(14.));\n        assert_eq!(theme.shadows.sm.len(), 2);\n        assert_eq!(theme.shadows.md.len(), 2);\n        assert_eq!(theme.shadows.lg.len(), 2);\n    }\n\n    #[gpui_kit::test]\n    fn keeps_component_state_tokens_in_sync_with_ui_theme(cx: &mut gpui_kit::TestAppContext) {\n        cx.update(|cx| {\n            init(cx);\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.mode, gpui_kit::component::ThemeMode::Light);\n            assert_eq!(\n                component.button_hover,\n                UiTheme::read(cx).colors.accent.into()\n            );\n\n            UiTheme::switch(cx, ThemeMode::Dark);\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.mode, gpui_kit::component::ThemeMode::Dark);\n            assert_eq!(component.ring, UiTheme::read(cx).colors.ring.into());\n\n            let mut custom = UiTheme::read(cx).clone();\n            custom.colors.accent = srgb(0x22, 0x66, 0xaa);\n            custom.fonts.body = \"Custom Sans\".into();\n            custom.radius = UiRadius::new(px(12.));\n            let expected_accent = custom.colors.accent;\n            UiTheme::set(cx, custom);\n\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.button_hover, expected_accent.into());\n            assert_eq!(component.font_family, \"Custom Sans\");\n            assert_eq!(component.radius, px(9.6));\n            assert_eq!(\n                component.tokens.button_hover.background,\n                expected_accent.into()\n            );\n\n            init(cx);\n            assert_eq!(\n                gpui_kit::component::Theme::global(cx).button_hover,\n                expected_accent.into()\n            );\n        });\n    }\n}\n\n// Refine the actual control after default styling; no extra layout or focus node.\n#[doc(hidden)]\npub fn apply_style<T: Styled>(mut element: T, style: &gpui_kit::StyleRefinement) -> T {\n    use gpui_kit::Refineable as _;\n    element.style().refine(style);\n    element\n}\n\n#[derive(gpui_kit::IntoElement)]\n#[doc(hidden)]\npub struct DisclosureIcon {\n    icon: gpui_kit::Svg,\n    open: bool,\n}\n#[doc(hidden)]\npub fn disclosure_icon(icon: gpui_kit::Svg, open: bool) -> DisclosureIcon {\n    DisclosureIcon { icon, open }\n}\nimpl gpui_kit::RenderOnce for DisclosureIcon {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl gpui_kit::IntoElement {\n        let duration = UiTheme::read(cx).motion.fast;\n        let value = transition_value(\n            \"disclosure-rotation\",\n            if self.open { 1. } else { 0. },\n            duration,\n            window,\n            cx,\n        );\n        self.icon\n            .with_transformation(gpui_kit::Transformation::rotate(gpui_kit::radians(\n                value * std::f32::consts::PI,\n            )))\n    }\n}\n\n#[cfg(test)]\nmod motion_tests {\n    use super::*;\n    use gpui_kit::{\n        AppContext as _, Context, IntoElement, Render, TestAppContext, VisualTestContext, div,\n    };\n\n    struct View {\n        target: f32,\n        sampled: f32,\n    }\n    impl Render for View {\n        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n            self.sampled = transition_value(\n                \"test-motion\",\n                self.target,\n                Duration::from_millis(200),\n                window,\n                cx,\n            );\n            div()\n        }\n    }\n\n    #[gpui_kit::test]\n    fn theme_reduced_motion_snaps_an_active_upstream_transition(cx: &mut TestAppContext) {\n        cx.update(init);\n        let window = cx.add_window(|_, _| View {\n            target: 0.,\n            sampled: 0.,\n        });\n        let mut visual = VisualTestContext::from_window(window.into(), cx);\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        window\n            .update(cx, |view, _, cx| {\n                view.target = 1.;\n                cx.notify();\n            })\n            .unwrap();\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert_eq!(\n            cx.read_window(&window, |view, cx| view.read(cx).sampled)\n                .unwrap(),\n            0.\n        );\n        cx.update(|cx| {\n            let mut theme = UiTheme::read(cx).clone();\n            theme.motion.reduced = true;\n            UiTheme::set(cx, theme);\n        });\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert_eq!(\n            cx.read_window(&window, |view, cx| view.read(cx).sampled)\n                .unwrap(),\n            1.\n        );\n    }\n}\n\n/// Shared focus traversal for the tab, radio and toggle collections.\n#[doc(hidden)]\npub struct RovingFocus {\n    pub handles: Vec<Option<gpui_kit::FocusHandle>>,\n    active: gpui_kit::Entity<Option<gpui_kit::FocusHandle>>,\n}\nimpl RovingFocus {\n    pub fn new(\n        id: gpui_kit::ElementId,\n        items: &[(gpui_kit::ElementId, bool)],\n        selected: Option<usize>,\n        window: &mut gpui_kit::Window,\n        cx: &mut App,\n    ) -> Self {\n        let active =\n            window.use_keyed_state((id, \"active\"), cx, |_, _| None::<gpui_kit::FocusHandle>);\n        let mut handles: Vec<_> = items\n            .iter()\n            .map(|(id, disabled)| {\n                let handle = window\n                    .use_keyed_state((id.clone(), \"focus\"), cx, |_, cx| cx.focus_handle())\n                    .read(cx)\n                    .clone();\n                handle.clone().tab_stop(false);\n                (!disabled).then_some(handle)\n            })\n            .collect();\n        let index = handles\n            .iter()\n            .position(|h| h.as_ref().is_some_and(|h| h.is_focused(window)))\n            .or(selected.filter(|&i| handles.get(i).is_some_and(Option::is_some)))\n            .or_else(|| {\n                handles\n                    .iter()\n                    .position(|h| h.is_some() && h.as_ref() == active.read(cx).as_ref())\n            })\n            .or_else(|| handles.iter().position(Option::is_some));\n        if let Some(index) = index {\n            let handle = handles[index].as_ref().unwrap().clone().tab_stop(true);\n            handles[index] = Some(handle.clone());\n            active.update(cx, |value, _| *value = Some(handle));\n        }\n        Self { handles, active }\n    }\n    pub fn key(\n        &self,\n        event: &gpui_kit::KeyDownEvent,\n        axis: Option<gpui_kit::Axis>,\n        window: &mut gpui_kit::Window,\n        cx: &mut App,\n    ) -> Option<usize> {\n        if event.keystroke.modifiers.modified() {\n            return None;\n        }\n        let enabled: Vec<_> = self\n            .handles\n            .iter()\n            .enumerate()\n            .filter_map(|(i, h)| h.as_ref().map(|h| (i, h)))\n            .collect();\n        let current = enabled.iter().position(|(_, h)| h.is_focused(window))?;\n        let next = match event.keystroke.key.as_str() {\n            \"home\" => 0,\n            \"end\" => enabled.len() - 1,\n            \"left\" if axis != Some(gpui_kit::Axis::Vertical) => {\n                (current + enabled.len() - 1) % enabled.len()\n            }\n            \"right\" if axis != Some(gpui_kit::Axis::Vertical) => (current + 1) % enabled.len(),\n            \"up\" if axis != Some(gpui_kit::Axis::Horizontal) => {\n                (current + enabled.len() - 1) % enabled.len()\n            }\n            \"down\" if axis != Some(gpui_kit::Axis::Horizontal) => (current + 1) % enabled.len(),\n            _ => return None,\n        };\n        enabled[current].1.clone().tab_stop(false);\n        let handle = enabled[next].1.clone().tab_stop(true);\n        handle.focus(window, cx);\n        self.active.update(cx, |value, _| *value = Some(handle));\n        window.refresh();\n        cx.stop_propagation();\n        Some(enabled[next].0)\n    }\n}\n"
    },
    {
      "path": "registry/button/button.rs",
      "type": "registry:file",
      "target": "~/src/ui/button.rs",
      "content": "//! The shadcn Nova Button visual port.\n//!\n//! Visual source: shadcn/ui 4.19.0 `button.tsx` and `style-nova.css` at\n//! `1773ecfeeb4a04366978d353e69b5c7ded78dcb2`. Interaction comes from the\n//! GPUI Kit headless button.\n\nuse std::rc::Rc;\n\nuse gpui_kit::base::Button as BaseButton;\nuse gpui_kit::{\n    AnyElement, App, ClickEvent, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce,\n    SharedString, Styled, Window, px,\n};\n\nuse super::theme::{\n    UiRadius, UiTheme,\n    primitives::{disabled as disabled_style, focus},\n};\n\ntype ButtonClickHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;\ntype ButtonStyleHandler = Rc<dyn Fn(BaseButton) -> BaseButton>;\n\n/// The pinned shadcn Button visual variant.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum ButtonVariant {\n    /// Primary action.\n    #[default]\n    Default,\n    /// Bordered action.\n    Outline,\n    /// Secondary action.\n    Secondary,\n    /// Low-emphasis action.\n    Ghost,\n    /// Destructive action.\n    Destructive,\n    /// Link-styled action.\n    Link,\n}\n\n/// The pinned shadcn Button size.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum ButtonSize {\n    /// 24px-high extra-small button.\n    Xs,\n    /// 28px-high small button.\n    Sm,\n    /// 32px-high default button.\n    #[default]\n    Default,\n    /// 36px-high large button.\n    Lg,\n    /// 24px square icon button.\n    IconXs,\n    /// 28px square icon button.\n    IconSm,\n    /// 32px square icon button.\n    Icon,\n    /// 36px square icon button.\n    IconLg,\n}\n\n#[derive(Clone, Copy, Debug, PartialEq)]\nstruct ButtonMetrics {\n    height: f32,\n    horizontal_padding: f32,\n    gap: f32,\n    radius: f32,\n    text_size: f32,\n    icon_only: bool,\n}\n\nimpl ButtonSize {\n    fn metrics(self, radii: UiRadius) -> ButtonMetrics {\n        let radius = f32::from(radii.lg);\n        let radius_md = f32::from(radii.md);\n        match self {\n            Self::Xs => ButtonMetrics::new(24.0, 8.0, 4.0, radius_md.min(10.0), 12.0, false),\n            Self::Sm => ButtonMetrics::new(28.0, 10.0, 4.0, radius_md.min(12.0), 12.8, false),\n            Self::Default => ButtonMetrics::new(32.0, 10.0, 6.0, radius, 14.0, false),\n            Self::Lg => ButtonMetrics::new(36.0, 10.0, 6.0, radius, 14.0, false),\n            Self::IconXs => ButtonMetrics::new(24.0, 0.0, 0.0, radius_md.min(10.0), 14.0, true),\n            Self::IconSm => ButtonMetrics::new(28.0, 0.0, 0.0, radius_md.min(12.0), 14.0, true),\n            Self::Icon => ButtonMetrics::new(32.0, 0.0, 0.0, radius, 14.0, true),\n            Self::IconLg => ButtonMetrics::new(36.0, 0.0, 0.0, radius, 14.0, true),\n        }\n    }\n}\n\nimpl ButtonMetrics {\n    const fn new(\n        height: f32,\n        horizontal_padding: f32,\n        gap: f32,\n        radius: f32,\n        text_size: f32,\n        icon_only: bool,\n    ) -> Self {\n        Self {\n            height,\n            horizontal_padding,\n            gap,\n            radius,\n            text_size,\n            icon_only,\n        }\n    }\n}\n\n/// A styled Button that keeps GPUI Kit's pointer and keyboard behavior.\n#[derive(IntoElement)]\npub struct Button {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    variant: ButtonVariant,\n    size: ButtonSize,\n    disabled: bool,\n    aria_label: Option<SharedString>,\n    on_click: Option<ButtonClickHandler>,\n    style_with: Option<ButtonStyleHandler>,\n    children: Vec<AnyElement>,\n}\n\nimpl Button {\n    /// Creates a Button with a caller-owned stable ID.\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            variant: ButtonVariant::default(),\n            size: ButtonSize::default(),\n            disabled: false,\n            aria_label: None,\n            on_click: None,\n            style_with: None,\n            children: Vec::new(),\n        }\n    }\n\n    /// Sets the visual variant.\n    pub fn variant(mut self, variant: ButtonVariant) -> Self {\n        self.variant = variant;\n        self\n    }\n\n    /// Sets the visual size.\n    pub fn size(mut self, size: ButtonSize) -> Self {\n        self.size = size;\n        self\n    }\n\n    /// Prevents activation and removes the Button from tab order.\n    pub fn disabled(mut self, disabled: bool) -> Self {\n        self.disabled = disabled;\n        self\n    }\n\n    /// Sets the accessible name for icon or custom content. Use `label` for plain text.\n    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {\n        self.aria_label = Some(label.into());\n        self\n    }\n\n    /// Adds visible text and uses it as the accessible name unless one was supplied.\n    pub fn label(mut self, label: impl Into<SharedString>) -> Self {\n        let label = label.into();\n        self.aria_label.get_or_insert_with(|| label.clone());\n        self.children.push(label.into_any_element());\n        self\n    }\n\n    /// Runs for pointer, Enter, or Space activation.\n    pub fn on_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_click = Some(Rc::new(handler));\n        self\n    }\n\n    /// Replaces the visual styling while retaining button interaction.\n    /// Include a visible keyboard focus treatment in custom styles.\n    pub fn style_with(mut self, handler: impl Fn(BaseButton) -> BaseButton + 'static) -> Self {\n        self.style_with = Some(Rc::new(handler));\n        self\n    }\n}\n\nimpl ParentElement for Button {\n    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {\n        self.children.extend(elements);\n    }\n}\n\nimpl RenderOnce for Button {\n    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let variant = self.variant;\n        let size = self.size;\n        let root = BaseButton::new(self.id).disabled(self.disabled);\n        let root = match self.style_with {\n            Some(style) => style(root),\n            None => style_button(root, self.disabled, variant, size, &theme),\n        };\n        let mut root = super::theme::apply_style(root, &self.style).children(self.children);\n\n        if let Some(label) = self.aria_label {\n            root = root.accessibility_label(label);\n        }\n        if let Some(handler) = self.on_click {\n            root = root.on_click(move |event, window, cx| handler(event, window, cx));\n        }\n\n        root\n    }\n}\n\n#[doc(hidden)]\npub fn style_button<T: Styled + gpui_kit::InteractiveElement + gpui_kit::prelude::FluentBuilder>(\n    base: T,\n    disabled: bool,\n    variant: ButtonVariant,\n    size: ButtonSize,\n    theme: &UiTheme,\n) -> T {\n    let colors = theme.colors;\n    let control = theme.control();\n    let metrics = size.metrics(theme.radius);\n    let invalid_ring = variant == ButtonVariant::Destructive;\n    let focus_theme = theme.clone();\n\n    let base = base\n        .flex()\n        .flex_shrink_0()\n        .items_center()\n        .justify_center()\n        .whitespace_nowrap()\n        .h(theme.space(metrics.height / 4.))\n        .gap(theme.space(metrics.gap / 4.))\n        .rounded(px(metrics.radius))\n        .border_1()\n        .border_color(colors.background.opacity(0.0))\n        .font_family(theme.fonts.body.clone())\n        .font_weight(FontWeight::MEDIUM)\n        .text_size(theme.text(metrics.text_size))\n        .focus_visible(move |style| focus::ring_for(style, &focus_theme, invalid_ring))\n        .when(metrics.icon_only, |base| {\n            base.w(theme.space(metrics.height / 4.)).p_0()\n        })\n        .when(!metrics.icon_only, |base| {\n            base.px(theme.space(metrics.horizontal_padding / 4.))\n        });\n\n    let base = match variant {\n        ButtonVariant::Default => base\n            .bg(colors.primary)\n            .text_color(colors.primary_foreground)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(colors.primary.opacity(0.80)))\n            }),\n        ButtonVariant::Outline => base\n            .bg(control.background)\n            .text_color(colors.foreground)\n            .border_color(control.border)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(control.hover).text_color(colors.foreground))\n            }),\n        ButtonVariant::Secondary => {\n            let hover = colors\n                .secondary\n                .blend(colors.secondary_foreground.opacity(0.08));\n            base.bg(colors.secondary)\n                .text_color(colors.secondary_foreground)\n                .when(!disabled, |base| base.hover(move |style| style.bg(hover)))\n        }\n        ButtonVariant::Ghost => base\n            .bg(colors.background.opacity(0.0))\n            .text_color(colors.foreground)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(control.ghost_hover).text_color(colors.foreground))\n            }),\n        ButtonVariant::Destructive => base\n            .bg(control.destructive_background)\n            .text_color(colors.destructive)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(control.destructive_hover))\n            }),\n        ButtonVariant::Link => base\n            .bg(colors.background.opacity(0.0))\n            .text_color(colors.primary)\n            .when(!disabled, |base| base.hover(|style| style.underline())),\n    };\n\n    disabled_style::interactive(base, disabled)\n}\n\nimpl gpui_kit::Styled for Button {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn all_variants_expose_clickable_and_disabled_cursors() {\n        for theme in [UiTheme::neutral_light(), UiTheme::neutral_dark()] {\n            for variant in [\n                ButtonVariant::Default,\n                ButtonVariant::Outline,\n                ButtonVariant::Secondary,\n                ButtonVariant::Ghost,\n                ButtonVariant::Destructive,\n                ButtonVariant::Link,\n            ] {\n                for disabled in [false, true] {\n                    let mut button = style_button(\n                        gpui_kit::div(),\n                        disabled,\n                        variant,\n                        ButtonSize::Default,\n                        &theme,\n                    );\n                    assert_eq!(\n                        button.style().mouse_cursor,\n                        Some(if disabled {\n                            gpui_kit::CursorStyle::OperationNotAllowed\n                        } else {\n                            gpui_kit::CursorStyle::PointingHand\n                        })\n                    );\n                }\n            }\n        }\n    }\n\n    #[test]\n    fn nova_sizes_are_exact() {\n        let sizes = [\n            (ButtonSize::Xs, 24.0, 8.0, 4.0, 8.0),\n            (ButtonSize::Sm, 28.0, 10.0, 4.0, 8.0),\n            (ButtonSize::Default, 32.0, 10.0, 6.0, 10.0),\n            (ButtonSize::Lg, 36.0, 10.0, 6.0, 10.0),\n            (ButtonSize::IconXs, 24.0, 0.0, 0.0, 8.0),\n            (ButtonSize::IconSm, 28.0, 0.0, 0.0, 8.0),\n            (ButtonSize::Icon, 32.0, 0.0, 0.0, 10.0),\n            (ButtonSize::IconLg, 36.0, 0.0, 0.0, 10.0),\n        ];\n\n        for (size, height, padding, gap, radius) in sizes {\n            let metrics = size.metrics(UiRadius::new(px(10.0)));\n            assert_eq!(metrics.height, height);\n            assert_eq!(metrics.horizontal_padding, padding);\n            assert_eq!(metrics.gap, gap);\n            assert_eq!(metrics.radius, radius);\n        }\n    }\n}\n"
    },
    {
      "path": "registry/menu/menu.rs",
      "type": "registry:file",
      "target": "~/src/ui/menu.rs",
      "content": "//! Nova menus composed from Kit buttons and popup positioning.\nuse super::{\n    button::{ButtonSize, ButtonVariant, style_button},\n    theme::{\n        UiTheme, apply_style,\n        primitives::{disabled, surface},\n    },\n};\nuse gpui_icons::{LucideIcon, lucide};\nuse gpui_kit::base::TestSupportExt as _;\nuse gpui_kit::base::{Align, Button, ElementExt as _, Placement, Positioner};\nuse gpui_kit::{\n    AnyElement, App, Bounds, ClickEvent, Context, ElementId, Entity, FocusHandle, Focusable,\n    InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Pixels, Point, Render,\n    RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,\n    Window, div, prelude::FluentBuilder as _,\n};\nuse std::{rc::Rc, time::Duration};\n\ntype Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;\n#[derive(Clone)]\n/// An action, check item, radio item, separator or submenu entry.\npub struct MenuItem {\n    id: ElementId,\n    label: SharedString,\n    disabled: bool,\n    checked: Option<bool>,\n    radio: bool,\n    link: bool,\n    handler: Option<Handler>,\n    children: Vec<MenuItem>,\n    separator: bool,\n}\nimpl MenuItem {\n    /// Creates an action with a caller-owned ID, unique within this menu tree.\n    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {\n        Self {\n            id: id.into(),\n            label: label.into(),\n            disabled: false,\n            checked: None,\n            radio: false,\n            link: false,\n            handler: None,\n            children: Vec::new(),\n            separator: false,\n        }\n    }\n    /// Disables interaction and applies the disabled appearance.\n    pub fn disabled(mut self, value: bool) -> Self {\n        self.disabled = value;\n        self\n    }\n    /// Makes this a checkbox item with the supplied initial state.\n    pub fn checked(mut self, value: bool) -> Self {\n        self.checked = Some(value);\n        self\n    }\n    /// Makes this a radio item; activating it clears radio siblings at this menu level.\n    pub fn radio(mut self, value: bool) -> Self {\n        self.checked = Some(value);\n        self.radio = true;\n        self\n    }\n    /// Handles activation by pointer, keyboard or an accessibility action.\n    pub fn on_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.handler = Some(Rc::new(handler));\n        self\n    }\n    /// Marks a link; the caller handles navigation through `on_click`.\n    pub fn link(mut self) -> Self {\n        self.link = true;\n        self\n    }\n    /// Creates an item containing another level of menu entries.\n    pub fn submenu(\n        id: impl Into<ElementId>,\n        label: impl Into<SharedString>,\n        items: impl IntoIterator<Item = MenuItem>,\n    ) -> Self {\n        let mut item = Self::new(id, label);\n        item.children = items.into_iter().collect();\n        item\n    }\n    /// Creates a non-interactive separator between menu entries.\n    pub fn separator() -> Self {\n        let mut item = Self::new(\"separator\", \"\");\n        item.separator = true;\n        item\n    }\n    /// Creates a non-interactive menu heading.\n    pub fn label(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {\n        Self::new(id, label).disabled(true)\n    }\n    fn enabled(&self) -> bool {\n        !self.disabled && !self.separator\n    }\n}\n\n/// A committed leaf action, including its new checked state when applicable.\n#[derive(Clone, Debug)]\npub struct MenuEvent {\n    /// Indices from the root menu to the activated leaf.\n    pub path: Vec<usize>,\n    /// The committed checked state, or `None` for a plain action.\n    pub checked: Option<bool>,\n}\nimpl gpui_kit::EventEmitter<MenuEvent> for MenuState {}\n\n/// Retained popup visibility, item state and keyboard navigation. Emits `MenuEvent` on leaf activation.\npub struct MenuState {\n    items: Vec<MenuItem>,\n    open: bool,\n    focus: FocusHandle,\n    trigger: FocusHandle,\n    return_focus: Option<FocusHandle>,\n    anchor: Bounds<Pixels>,\n    pointer: bool,\n    path: Vec<usize>,\n    highlighted: Vec<Option<usize>>,\n    row_bounds: Vec<Vec<Bounds<Pixels>>>,\n    popup_bounds: Vec<Bounds<Pixels>>,\n    query: String,\n    typed_at: Option<web_time::Instant>,\n    submenu_timer: Option<gpui_kit::Task<()>>,\n}\n\n/// How long the pointer rests on a submenu row before the submenu opens.\nconst SUBMENU_HOVER_DELAY: Duration = Duration::from_millis(200);\n/// The gap kept between a menu panel and the window edge.\nconst WINDOW_MARGIN: f32 = 8.;\n/// The shortest a menu panel gets before it scrolls in a very short window.\nconst MIN_PANEL_HEIGHT: f32 = 96.;\n\n/// The tallest a menu panel can be before it scrolls: the larger of the room\n/// below and above `anchor` for a menu that drops from its trigger, or the\n/// window height for one beside its anchor (a context menu or a submenu),\n/// which slides up and down instead of flipping.\n#[doc(hidden)]\npub fn menu_max_height(\n    anchor: Bounds<Pixels>,\n    viewport: gpui_kit::Size<Pixels>,\n    beside: bool,\n    offset: Pixels,\n) -> Pixels {\n    let margin = gpui_kit::px(WINDOW_MARGIN);\n    let room = if beside {\n        viewport.height - margin * 2.\n    } else {\n        let below = viewport.height - anchor.bottom() - offset - margin;\n        let above = anchor.top() - offset - margin;\n        below.max(above)\n    };\n    room.max(gpui_kit::px(MIN_PANEL_HEIGHT))\n}\nimpl MenuState {\n    /// Creates a closed menu with the supplied entries and new focus handles.\n    pub fn new(items: impl IntoIterator<Item = MenuItem>, cx: &mut Context<Self>) -> Self {\n        Self {\n            items: items.into_iter().collect(),\n            open: false,\n            focus: cx.focus_handle().tab_stop(false),\n            trigger: cx.focus_handle().tab_stop(true),\n            return_focus: None,\n            anchor: Default::default(),\n            pointer: false,\n            path: Vec::new(),\n            highlighted: Vec::new(),\n            row_bounds: Vec::new(),\n            popup_bounds: Vec::new(),\n            query: String::new(),\n            typed_at: None,\n            submenu_timer: None,\n        }\n    }\n    /// Returns whether the popup is open.\n    pub fn is_open(&self) -> bool {\n        self.open\n    }\n    /// Replaces the menu entries and resets submenu navigation.\n    pub fn set_items(&mut self, items: impl IntoIterator<Item = MenuItem>, cx: &mut Context<Self>) {\n        self.items = items.into_iter().collect();\n        self.path.clear();\n        self.highlighted = vec![self.items.iter().position(MenuItem::enabled)];\n        cx.notify();\n    }\n    /// Returns the handle used by the menu trigger.\n    pub fn trigger_focus(&self) -> FocusHandle {\n        self.trigger.clone()\n    }\n    /// Opens the menu at its trigger and transfers keyboard focus into it.\n    pub fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {\n        self.open_at(None, window, cx);\n    }\n    /// Opens at a pointer position, or at the trigger when the position is absent.\n    pub fn open_at(\n        &mut self,\n        point: Option<Point<Pixels>>,\n        window: &mut Window,\n        cx: &mut Context<Self>,\n    ) {\n        self.pointer = point.is_some();\n        self.return_focus = if self.pointer {\n            window.focused(cx)\n        } else {\n            None\n        };\n        if let Some(point) = point {\n            self.anchor = Bounds::new(point, gpui_kit::size(gpui_kit::px(0.), gpui_kit::px(0.)));\n        }\n        self.open = true;\n        self.path.clear();\n        self.query.clear();\n        self.highlighted = vec![self.items.iter().position(MenuItem::enabled)];\n        self.focus.focus(window, cx);\n        cx.notify();\n    }\n    /// Closes and optionally restores the prior context focus or menu trigger.\n    pub fn close(&mut self, restore: bool, window: &mut Window, cx: &mut Context<Self>) {\n        self.open = false;\n        self.path.clear();\n        if restore {\n            self.return_focus\n                .take()\n                .unwrap_or_else(|| self.trigger.clone())\n                .focus(window, cx);\n        }\n        cx.notify();\n    }\n    #[doc(hidden)]\n    pub fn submenu_key(&self, right: bool) -> bool {\n        if !right {\n            return !self.path.is_empty();\n        }\n        self.highlighted\n            .last()\n            .copied()\n            .flatten()\n            .is_some_and(|i| !self.level(self.path.len())[i].children.is_empty())\n    }\n    fn level(&self, depth: usize) -> &[MenuItem] {\n        let mut items = self.items.as_slice();\n        for &index in self.path.iter().take(depth) {\n            items = &items[index].children;\n        }\n        items\n    }\n    fn highlight(&mut self, depth: usize, index: usize, cx: &mut Context<Self>) {\n        if !self.level(depth)[index].enabled() {\n            return;\n        }\n        if self.highlighted.get(depth).copied().flatten() == Some(index) {\n            return;\n        }\n        self.path.truncate(depth);\n        self.highlighted.truncate(depth + 1);\n        self.highlighted[depth] = Some(index);\n        cx.notify();\n    }\n    /// Opens the submenu at `index` after a short rest, unless the pointer\n    /// has moved on to another row by then.\n    fn hover_submenu(\n        &mut self,\n        depth: usize,\n        index: usize,\n        window: &mut Window,\n        cx: &mut Context<Self>,\n    ) {\n        self.submenu_timer = None;\n        if self.level(depth)[index].children.is_empty() || self.path.get(depth) == Some(&index) {\n            return;\n        }\n        self.submenu_timer = Some(cx.spawn_in(window, async move |this, cx| {\n            cx.background_executor().timer(SUBMENU_HOVER_DELAY).await;\n            _ = this.update(cx, |this, cx| {\n                if this.open\n                    && this.path.len() == depth\n                    && this.highlighted.get(depth).copied().flatten() == Some(index)\n                {\n                    this.open_submenu(depth, index, cx);\n                }\n            });\n        }));\n    }\n    fn open_submenu(&mut self, depth: usize, index: usize, cx: &mut Context<Self>) {\n        let first = self.level(depth)[index]\n            .children\n            .iter()\n            .position(MenuItem::enabled);\n        self.path.truncate(depth);\n        self.path.push(index);\n        self.highlighted.truncate(depth + 1);\n        self.highlighted.push(first);\n        cx.notify();\n    }\n    fn activate(\n        &mut self,\n        depth: usize,\n        index: usize,\n        event: &ClickEvent,\n        window: &mut Window,\n        cx: &mut Context<Self>,\n    ) {\n        let item = self.level(depth)[index].clone();\n        if !item.enabled() {\n            return;\n        }\n        if !item.children.is_empty() {\n            self.submenu_timer = None;\n            self.open_submenu(depth, index, cx);\n        } else {\n            let mut path = self.path[..depth].to_vec();\n            path.push(index);\n            let checked = item.checked.map(|value| item.radio || !value);\n            if let Some(checked) = checked {\n                let mut items = &mut self.items;\n                for &parent in &path[..depth] {\n                    items = &mut items[parent].children;\n                }\n                if item.radio {\n                    for sibling in items.iter_mut().filter(|item| item.radio) {\n                        sibling.checked = Some(false);\n                    }\n                }\n                items[index].checked = Some(checked);\n            }\n            self.close(true, window, cx);\n            cx.emit(MenuEvent { path, checked });\n            if let Some(handler) = item.handler {\n                handler(event, window, cx);\n            }\n        }\n    }\n    fn key(&mut self, event: &gpui_kit::KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {\n        if !self.open {\n            return;\n        }\n        let depth = self.path.len();\n        let key = event.keystroke.key.as_str();\n        if key == \"escape\" {\n            self.close(true, window, cx);\n        } else if key == \"tab\" {\n            self.close(true, window, cx);\n            return;\n        } else if event.keystroke.modifiers.modified() {\n            return;\n        } else if key == \"left\" && depth > 0 {\n            self.path.pop();\n            self.highlighted.pop();\n            cx.notify();\n        } else if matches!(key, \"enter\" | \"space\" | \"right\") {\n            if let Some(index) = self.highlighted[depth]\n                && (key != \"right\" || !self.level(depth)[index].children.is_empty())\n            {\n                self.activate(\n                    depth,\n                    index,\n                    &ClickEvent::Keyboard(Default::default()),\n                    window,\n                    cx,\n                );\n            }\n        } else {\n            let items = self.level(depth);\n            let enabled: Vec<_> = items\n                .iter()\n                .enumerate()\n                .filter_map(|(i, item)| item.enabled().then_some(i))\n                .collect();\n            if enabled.is_empty() {\n                return;\n            }\n            let current = enabled\n                .iter()\n                .position(|i| Some(*i) == self.highlighted[depth])\n                .unwrap_or(0);\n            let next = match key {\n                \"up\" => Some(enabled[(current + enabled.len() - 1) % enabled.len()]),\n                \"down\" => Some(enabled[(current + 1) % enabled.len()]),\n                \"home\" => enabled.first().copied(),\n                \"end\" => enabled.last().copied(),\n                _ if key.chars().count() == 1 => {\n                    let now = web_time::Instant::now();\n                    if self\n                        .typed_at\n                        .is_none_or(|time| now.duration_since(time) > Duration::from_millis(700))\n                    {\n                        self.query.clear();\n                    }\n                    self.typed_at = Some(now);\n                    self.query.push_str(key);\n                    let query = self.query.to_lowercase();\n                    enabled.into_iter().find(|&i| {\n                        self.level(depth)[i]\n                            .label\n                            .to_lowercase()\n                            .starts_with(&query)\n                    })\n                }\n                _ => return,\n            };\n            if let Some(index) = next {\n                self.highlight(depth, index, cx);\n            }\n        }\n        cx.stop_propagation();\n    }\n}\nimpl Focusable for MenuState {\n    fn focus_handle(&self, _: &App) -> FocusHandle {\n        self.focus.clone()\n    }\n}\nimpl Render for MenuState {\n    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n        let t = UiTheme::read(cx).clone();\n        let viewport = window.viewport_size();\n        let mut root = div()\n            .id((\"menu\", cx.entity_id()))\n            .track_focus(&self.focus)\n            .capture_key_down(cx.listener(Self::key));\n        if !self.open {\n            return root;\n        }\n        self.row_bounds.resize_with(self.path.len() + 1, Vec::new);\n        self.popup_bounds\n            .resize(self.path.len() + 1, Default::default());\n        for depth in 0..=self.path.len() {\n            let items = self.level(depth).to_vec();\n            self.row_bounds[depth].resize(items.len(), Default::default());\n            let entity = cx.entity();\n            let popup_id = if depth == 0 {\n                ElementId::from((\"menu-popup\", cx.entity_id()))\n            } else {\n                (\n                    self.level(depth - 1)[self.path[depth - 1]].id.clone(),\n                    \"popup\",\n                )\n                    .into()\n            };\n            let anchor = if depth == 0 {\n                self.anchor\n            } else {\n                self.row_bounds[depth - 1][self.path[depth - 1]]\n            };\n            let beside = depth > 0 || self.pointer;\n            let mut popup = div()\n                .id(popup_id)\n                .relative()\n                .role(Role::Menu)\n                .min_w(t.space(40.))\n                .max_h(menu_max_height(anchor, viewport, beside, t.space(1.)))\n                .debug_selector(move || format!(\"menu-popup-{depth}\"))\n                .overflow_y_scroll()\n                .map(|popup| surface::menu(popup, &t))\n                .on_prepaint(move |bounds, _, cx| {\n                    entity.update(cx, |this, _| this.popup_bounds[depth] = bounds)\n                })\n                .on_mouse_down_out(cx.listener(\n                    |this, event: &gpui_kit::MouseDownEvent, window, cx| {\n                        if !this.anchor.contains(&event.position)\n                            && !this\n                                .popup_bounds\n                                .iter()\n                                .any(|b| b.contains(&event.position))\n                        {\n                            this.close(false, window, cx);\n                        }\n                    },\n                ));\n            for (index, item) in items.into_iter().enumerate() {\n                if item.separator {\n                    popup = popup.child(\n                        div()\n                            .h(gpui_kit::px(1.))\n                            .my(t.space(1.))\n                            .bg(t.colors.border),\n                    );\n                    continue;\n                }\n                let entity = cx.entity();\n                let selected = self.highlighted[depth] == Some(index);\n                let submenu = !item.children.is_empty();\n                let role = if submenu {\n                    Role::MenuItem\n                } else if item.link {\n                    Role::Link\n                } else if item.radio {\n                    Role::MenuItemRadio\n                } else if item.checked.is_some() {\n                    Role::MenuItemCheckBox\n                } else {\n                    Role::MenuItem\n                };\n                let row = Button::new(item.id.clone())\n                    .role(role)\n                    .focusable(false)\n                    .disabled(item.disabled)\n                    .accessibility_label(item.label.clone())\n                    .relative()\n                    .w_full()\n                    .min_h(t.space(8.))\n                    .justify_start()\n                    .gap(t.space(2.))\n                    .px(t.space(1.5))\n                    .rounded(t.radius.sm)\n                    .when(selected, |b| b.bg(t.colors.accent).aria_active_descendant())\n                    .map(|b| disabled::dim(b, item.disabled))\n                    .when_some(item.checked, |b, checked| {\n                        b.aria_toggled(if checked {\n                            gpui_kit::Toggled::True\n                        } else {\n                            gpui_kit::Toggled::False\n                        })\n                    })\n                    .when(submenu, |b| {\n                        b.aria_expanded(self.path.get(depth) == Some(&index))\n                    })\n                    .on_prepaint(move |bounds, window, cx| {\n                        entity.update(cx, |this, _| {\n                            if this.row_bounds[depth][index] != bounds {\n                                this.row_bounds[depth][index] = bounds;\n                                window.request_animation_frame();\n                            }\n                        })\n                    })\n                    .on_hover(cx.listener(move |this, hovered, window, cx| {\n                        if *hovered {\n                            this.highlight(depth, index, cx);\n                            this.hover_submenu(depth, index, window, cx);\n                        }\n                    }))\n                    .on_click(cx.listener(move |this, event, window, cx| {\n                        this.activate(depth, index, event, window, cx)\n                    }))\n                    .child(div().flex_1().child(item.label))\n                    .when(item.checked == Some(true), |b| {\n                        b.child(\n                            lucide(LucideIcon::Check)\n                                .size(t.space(4.))\n                                .text_color(t.colors.popover_foreground),\n                        )\n                    })\n                    .when(submenu, |b| {\n                        b.child(\n                            lucide(LucideIcon::ChevronRight)\n                                .size(t.space(4.))\n                                .text_color(t.colors.popover_foreground),\n                        )\n                    });\n                popup = popup.child(row);\n            }\n            root = root.child(\n                gpui_kit::deferred(\n                    Positioner::side(anchor)\n                        .align(Align::Start)\n                        .margin(gpui_kit::px(WINDOW_MARGIN))\n                        .placement(if beside {\n                            Placement::Right\n                        } else {\n                            Placement::Bottom\n                        })\n                        .offset(t.space(1.))\n                        .occlude()\n                        .child(popup),\n                )\n                .with_priority(gpui_kit::base::POPUP_PRIORITY + depth),\n            );\n        }\n        root\n    }\n}\n\n#[derive(IntoElement)]\n/// A named menu trigger or context area backed by retained menu state.\npub struct Menu {\n    state: Entity<MenuState>,\n    label: SharedString,\n    disabled: bool,\n    context: Option<AnyElement>,\n    trigger: Option<AnyElement>,\n    trigger_style: Option<Box<dyn FnOnce(Button) -> Button>>,\n    style: StyleRefinement,\n}\nimpl Menu {\n    /// Creates a named trigger for the caller's retained menu state.\n    pub fn new(state: &Entity<MenuState>, label: impl Into<SharedString>) -> Self {\n        Self {\n            state: state.clone(),\n            label: label.into(),\n            disabled: false,\n            context: None,\n            trigger: None,\n            trigger_style: None,\n            style: Default::default(),\n        }\n    }\n    /// Supplies custom content for the named trigger button.\n    pub fn trigger(mut self, child: impl IntoElement) -> Self {\n        self.trigger = Some(child.into_any_element());\n        self\n    }\n    /// Replaces trigger styling while retaining menu behavior. Include a visible keyboard focus treatment.\n    pub fn trigger_style_with(mut self, style: impl FnOnce(Button) -> Button + 'static) -> Self {\n        self.trigger_style = Some(Box::new(style));\n        self\n    }\n    /// Disables interaction and applies the disabled appearance.\n    pub fn disabled(mut self, value: bool) -> Self {\n        self.disabled = value;\n        self\n    }\n    /// Replaces the button trigger with content that opens a menu on secondary click or Shift+F10.\n    pub fn context_area(mut self, child: impl IntoElement) -> Self {\n        self.context = Some(child.into_any_element());\n        self\n    }\n}\nimpl Styled for Menu {\n    fn style(&mut self) -> &mut StyleRefinement {\n        &mut self.style\n    }\n}\nimpl RenderOnce for Menu {\n    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {\n        let state = self.state.clone();\n        let keyboard = state.clone();\n        let measured = state.clone();\n        let context = self.context.is_some();\n        let disabled = self.disabled;\n        let focus = state.read(cx).trigger.clone();\n        let open = state.read(cx).open;\n        let mut base = div()\n            .id((\"menu-trigger\", state.entity_id()))\n            .test_support()\n            .relative()\n            .on_prepaint(move |bounds, _, cx| {\n                measured.update(cx, |this, _| {\n                    if !this.pointer || !this.open {\n                        this.anchor = bounds;\n                    }\n                })\n            })\n            .capture_key_down(move |event, window, cx| {\n                let key = event.keystroke.key.as_str();\n                if !disabled\n                    && keyboard.read(cx).trigger.is_focused(window)\n                    && ((!context && (key == \"down\" || key == \"up\"))\n                        || (context && key == \"f10\" && event.keystroke.modifiers.shift))\n                {\n                    keyboard.update(cx, |this, cx| this.open(window, cx));\n                    cx.stop_propagation();\n                }\n            });\n        if let Some(area) = self.context {\n            base = base\n                .track_focus(&focus.tab_stop(!disabled))\n                .aria_label(self.label)\n                // Keep the content's focus for selection and context-menu restoration.\n                .capture_any_mouse_down(|event, window, _| {\n                    if matches!(event.button, MouseButton::Left | MouseButton::Right) {\n                        window.prevent_default();\n                    }\n                })\n                .on_mouse_down(MouseButton::Right, move |event, window, cx| {\n                    if !disabled {\n                        state.update(cx, |this, cx| {\n                            this.open_at(Some(event.position), window, cx)\n                        });\n                        cx.stop_propagation();\n                    }\n                })\n                .child(area);\n        } else {\n            base = base.child(\n                Button::new((\"menu-button\", state.entity_id()))\n                    .track_focus(&focus)\n                    .tab_stop(focus.tab_stop)\n                    .disabled(disabled)\n                    .accessibility_label(self.label.clone())\n                    .aria_expanded(open)\n                    .map(|button| match self.trigger_style {\n                        Some(style) => style(button),\n                        None => style_button(\n                            button,\n                            disabled,\n                            ButtonVariant::Outline,\n                            ButtonSize::Default,\n                            UiTheme::read(cx),\n                        ),\n                    })\n                    .on_click(move |_, window, cx| {\n                        state.update(cx, |this, cx| {\n                            if this.open {\n                                this.close(true, window, cx);\n                            } else {\n                                this.open(window, cx);\n                            }\n                        })\n                    })\n                    .child(\n                        self.trigger\n                            .unwrap_or_else(|| self.label.into_any_element()),\n                    ),\n            );\n        }\n        apply_style(base.child(self.state), &self.style)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use gpui_kit::{AppContext as _, point, px, size};\n\n    #[test]\n    fn a_menu_uses_the_room_in_the_window() {\n        let viewport = size(px(1000.), px(800.));\n        let offset = px(4.);\n        // A trigger near the top drops a panel almost to the bottom edge.\n        let top = Bounds::new(point(px(10.), px(20.)), size(px(80.), px(32.)));\n        assert_eq!(\n            menu_max_height(top, viewport, false, offset),\n            px(800. - 52. - 4. - 8.)\n        );\n        // A trigger near the bottom has more room above, where it flips.\n        let bottom = Bounds::new(point(px(10.), px(700.)), size(px(80.), px(32.)));\n        assert_eq!(\n            menu_max_height(bottom, viewport, false, offset),\n            px(700. - 4. - 8.)\n        );\n        // A context menu or submenu slides along the window's full height.\n        assert_eq!(\n            menu_max_height(bottom, viewport, true, offset),\n            px(800. - 16.)\n        );\n        // A very short window still shows a few rows and scrolls.\n        let tiny = size(px(300.), px(60.));\n        assert_eq!(menu_max_height(top, tiny, false, offset), px(96.));\n    }\n\n    struct LongMenu {\n        state: Entity<MenuState>,\n    }\n    impl Render for LongMenu {\n        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {\n            div()\n                .p(px(16.))\n                .child(Menu::new(&self.state, \"Message actions\"))\n        }\n    }\n\n    #[gpui_kit::test]\n    fn a_long_menu_does_not_scroll_in_a_tall_window(cx: &mut gpui_kit::TestAppContext) {\n        cx.update(super::super::theme::init);\n        let (view, cx) = cx.add_window_view(|_, cx| LongMenu {\n            state: cx.new(|cx| {\n                MenuState::new(\n                    (0..12).map(|i| MenuItem::new((\"item\", i as u64), format!(\"Action {i}\"))),\n                    cx,\n                )\n            }),\n        });\n        cx.simulate_resize(size(px(800.), px(800.)));\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        cx.update(|window, cx| {\n            let state = view.read(cx).state.clone();\n            state.update(cx, |menu, cx| menu.open(window, cx));\n        });\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        let popup = cx.debug_bounds(\"menu-popup-0\").expect(\"the menu is open\");\n        // Twelve 32px rows plus padding: taller than the old 288px cap.\n        assert!(popup.size.height > px(12. * 32.), \"{popup:?}\");\n    }\n\n    #[gpui_kit::test]\n    fn resting_on_a_submenu_row_opens_it_and_a_sibling_closes_it(\n        cx: &mut gpui_kit::TestAppContext,\n    ) {\n        cx.update(super::super::theme::init);\n        let window = cx.add_window(|_, cx| {\n            MenuState::new(\n                [\n                    MenuItem::new(\"reply\", \"Reply\"),\n                    MenuItem::submenu(\n                        \"remind\",\n                        \"Remind me\",\n                        [\n                            MenuItem::new(\"hour\", \"In 1 hour\"),\n                            MenuItem::new(\"tomorrow\", \"Tomorrow\"),\n                        ],\n                    ),\n                    MenuItem::new(\"copy\", \"Copy link\"),\n                ],\n                cx,\n            )\n        });\n        window\n            .update(cx, |menu, window, cx| {\n                menu.open(window, cx);\n                menu.highlight(0, 1, cx);\n                menu.hover_submenu(0, 1, window, cx);\n            })\n            .unwrap();\n        cx.executor().advance_clock(SUBMENU_HOVER_DELAY / 2);\n        cx.run_until_parked();\n        window\n            .update(cx, |menu, _, _| {\n                assert!(menu.path.is_empty(), \"not before the delay\")\n            })\n            .unwrap();\n        cx.executor().advance_clock(SUBMENU_HOVER_DELAY);\n        cx.run_until_parked();\n        window\n            .update(cx, |menu, window, cx| {\n                assert_eq!(menu.path, [1], \"the submenu opened\");\n                menu.highlight(0, 2, cx);\n                menu.hover_submenu(0, 2, window, cx);\n                assert!(menu.path.is_empty(), \"a sibling row closes it\");\n                // Passing over the submenu row on the way elsewhere opens nothing.\n                menu.highlight(0, 1, cx);\n                menu.hover_submenu(0, 1, window, cx);\n                menu.highlight(0, 0, cx);\n                menu.hover_submenu(0, 0, window, cx);\n            })\n            .unwrap();\n        cx.executor().advance_clock(SUBMENU_HOVER_DELAY * 2);\n        cx.run_until_parked();\n        window\n            .update(cx, |menu, _, _| assert!(menu.path.is_empty()))\n            .unwrap();\n    }\n    #[gpui_kit::test]\n    fn check_actions_toggle_and_radio_actions_select_one(cx: &mut gpui_kit::TestAppContext) {\n        cx.update(super::super::theme::init);\n        let window = cx.add_window(|_, cx| {\n            MenuState::new(\n                [\n                    MenuItem::new(\"notifications\", \"Notifications\").checked(true),\n                    MenuItem::new(\"compact\", \"Compact\").radio(true),\n                    MenuItem::new(\"comfortable\", \"Comfortable\").radio(false),\n                    MenuItem::new(\"locked\", \"Locked\")\n                        .checked(true)\n                        .disabled(true),\n                ],\n                cx,\n            )\n        });\n        window\n            .update(cx, |menu, window, cx| {\n                menu.activate(0, 0, &ClickEvent::default(), window, cx);\n                assert_eq!(menu.items[0].checked, Some(false));\n                menu.activate(0, 2, &ClickEvent::default(), window, cx);\n                assert_eq!(menu.items[1].checked, Some(false));\n                assert_eq!(menu.items[2].checked, Some(true));\n                menu.activate(0, 3, &ClickEvent::default(), window, cx);\n                assert_eq!(menu.items[3].checked, Some(true));\n            })\n            .unwrap();\n    }\n}\n"
    },
    {
      "path": "registry/context_menu/context_menu.rs",
      "type": "registry:file",
      "target": "~/src/ui/context_menu.rs",
      "content": "//! Right-click and Shift-F10 menus using the shared Nova menu state.\nuse super::menu::{Menu, MenuState};\nuse gpui_kit::{Entity, IntoElement, SharedString};\n/// Attach a context menu to a focusable area. The caller owns its menu state.\npub fn context_menu(\n    state: &Entity<MenuState>,\n    label: impl Into<SharedString>,\n    area: impl IntoElement,\n) -> Menu {\n    Menu::new(state, label).context_area(area)\n}\n"
    }
  ]
}
