{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "collapsible",
  "type": "registry:item",
  "title": "Collapsible",
  "description": "The shadcn default Collapsible 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/collapsible/collapsible.rs",
      "type": "registry:file",
      "target": "~/src/ui/collapsible.rs",
      "content": "//! Nova presentation for GPUI Kit's controlled collapsible region.\nuse super::theme::UiTheme;\nuse gpui_kit::base::Button;\npub use gpui_kit::base::Collapsible;\nuse gpui_kit::{\n    App, Div, ElementId, InteractiveElement as _, StatefulInteractiveElement as _, Styled, Window,\n    div,\n};\n\n/// Creates a controlled region that reveals and removes its content with theme motion.\npub fn collapsible(\n    id: impl Into<ElementId>,\n    open: bool,\n    window: &mut Window,\n    cx: &mut App,\n) -> Collapsible {\n    let id = id.into();\n    let presence = super::theme::presence(id.clone(), open, window, cx);\n    let visible = presence.should_render() && (open || presence.progress > 0.);\n    let mut region = Collapsible::new().open(visible);\n    if visible {\n        region = region.reveal(id, presence.progress);\n    }\n    region\n        .flex()\n        .flex_col()\n        .font_family(UiTheme::read(cx).fonts.body.clone())\n}\n/// Creates an accessible trigger. Update the caller's open state in its click handler.\npub fn collapsible_trigger(id: impl Into<ElementId>, open: bool, cx: &App) -> Button {\n    let theme = UiTheme::read(cx);\n    let colors = theme.colors;\n    Button::new(id)\n        .aria_expanded(open)\n        .px(theme.space(2.5))\n        .py(theme.space(1.5))\n        .rounded(theme.radius.lg)\n        .border_1()\n        .border_color(colors.background.opacity(0.))\n        .text_size(theme.text(14.))\n        .text_color(colors.foreground)\n        .cursor_pointer()\n        .hover(move |s| s.bg(colors.muted))\n        .focus_visible(move |s| s.border_color(colors.ring))\n}\n/// Creates the disclosure content's text treatment.\npub fn collapsible_content(cx: &App) -> Div {\n    let theme = UiTheme::read(cx);\n    div()\n        .w_full()\n        .pt(theme.space(2.))\n        .overflow_hidden()\n        .font_family(theme.fonts.body.clone())\n        .text_size(theme.text(14.))\n        .text_color(theme.colors.foreground)\n}\n"
    }
  ]
}
