//
//  ComponentRegistry.swift
//  Demo
//  Created by İsa Mercan on 23.06.2026.
//
//  Single source of truth for the gallery. Adding a component is one entry:
//  `.knob("Name", .category, demo: NameDemo(), usage: #"..."#)` for an
//  interactive page, or `.static("Name", .category, usage:) { Component(...) }`.
//  `usage` is shown as a copyable code card on the component's page.
//

import SwiftUI
import ThemeKit

enum ComponentCategory: String, CaseIterable, Identifiable {
    case atoms = "Atoms"
    case molecules = "Molecules"
    case organisms = "Organisms"
    var id: String { rawValue }
}

struct ComponentEntry: Identifiable {
    let id = UUID()
    let name: String
    let category: ComponentCategory
    let usage: String?
    let make: () -> AnyView

    static func knob<D: View>(_ name: String, _ category: ComponentCategory, demo: @escaping @autoclosure () -> D, usage: String? = nil) -> ComponentEntry {
        ComponentEntry(name: name, category: category, usage: usage) { AnyView(demo()) }
    }

    static func `static`<P: View>(_ name: String, _ category: ComponentCategory, usage: String? = nil, @ViewBuilder preview: @escaping () -> P) -> ComponentEntry {
        ComponentEntry(name: name, category: category, usage: usage) { AnyView(ComponentStage(name) { preview() }) }
    }
}

/// Gives a `.static` gallery preview its own mutable `@State` so interactive
/// components (steppers, toggles) work without a bespoke Demo view.
struct StatefulPreview<Value, Content: View>: View {
    @State private var value: Value
    private let content: (Binding<Value>) -> Content
    init(_ initial: Value, @ViewBuilder content: @escaping (Binding<Value>) -> Content) {
        _value = State(initialValue: initial)
        self.content = content
    }
    var body: some View { content($value) }
}

/// Seat-map demo with a live multi-select set.
private struct SeatMapDemo: View {
    @State private var picked: Set<String> = ["12C"]
    private var rows: [[SeatSlot]] {
        (10...14).map { r in
            [.seat(Seat("\(r)A", premium: r == 10)), .seat(Seat("\(r)B")), .seat(Seat("\(r)C", occupied: r == 12)),
             .aisle,
             .seat(Seat("\(r)D")), .seat(Seat("\(r)E", occupied: r == 13)), .seat(Seat("\(r)F"))]
        }
    }
    var body: some View { SeatMap(rows: rows, selection: $picked).maxSelection(3) }
}

/// Two-binding demo for the price histogram + range filter.
private struct PriceHistogramDemo: View {
    @State private var low = 800.0
    @State private var high = 3_200.0
    var body: some View {
        PriceHistogram(bins: [2, 5, 9, 14, 18, 22, 19, 12, 8, 5, 3, 2],
                       lowerValue: $low, upperValue: $high, in: 0...5_000)
            .frame(maxWidth: 340)
    }
}

enum ComponentRegistry {
    static let all: [ComponentEntry] = [
        // MARK: Atoms
        .knob("Avatar", .atoms, demo: AvatarDemo(), usage: #"Avatar(.initials("AB")).size(.md).presence(.online)"#),
        .knob("Badge", .atoms, demo: BadgeDemo(), usage: #"Badge("Label").badgeStyle(.info).icon("star.fill")"#),
        .knob("Chip", .atoms, demo: ChipDemo(), usage: #"Chip("Recommended", isSelected: $selected).chipStyle(.tonal)"#),
        .knob("Color Palette", .atoms, demo: ColorLadderDemo(), usage: #"SemanticColor.primary.shade(.s500)   // base · .bg .hover .active .strong"#),
        .knob("CountBadge", .atoms, demo: CountBadgeDemo(), usage: ##"icon.countBadge(5)   //  .dotBadge() · Ribbon("New") { card }"##),
        .knob("BorderBeam", .atoms, demo: BorderBeamDemo(), usage: #"card.borderBeam(cornerRadius: 16, lineWidth: 2)"#),
        .knob("Divider", .atoms, demo: DividerDemo(), usage: #"DividerView("OR").dashed().titleAlign(.center)"#),
        .knob("Icon", .atoms, demo: IconDemo(), usage: #"Icon(systemName: "star.fill").size(.md).color(theme.foreground(.fgHero))"#),
        .knob("InputLabel", .atoms, demo: InputLabelDemo(), usage: #"InputLabel("Email").required().hasInfo()"#),
        .knob("ProgressBar", .atoms, demo: ProgressBarDemo(), usage: #"ProgressBar(value: 0.4).showsPercentage()"#),
        .knob("RadialProgress", .atoms, demo: RadialProgressDemo(), usage: #"RadialProgress(0.6).size(96).showsLabel()"#),
        .knob("RemoteImage", .atoms, demo: RemoteImageDemo(), usage: #"RemoteImage(url, ratio: "16:9").cornerRadius(12)   // .gif/.apng animate natively"#),
        .static("AnimatedImage", .atoms, usage: #"AnimatedImage(gifURL)   // GIF/APNG via ImageIO — no dependency"#) { AnimatedImage(URL(string: "https://upload.wikimedia.org/wikipedia/commons/d/d3/Newtons_cradle_animation_book_2.gif")).frame(width: 180, height: 180).clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) },
        .knob("RollingNumber", .atoms, demo: RollingNumberDemo(), usage: #"RollingNumber(1284).size(40)   // odometer digit roll"#),
        .knob("Indicator", .atoms, demo: IndicatorDemo(), usage: #"icon.indicatorDot()   // or .indicator { Badge("3") }"#),
        .static("Kbd", .atoms, usage: #"Kbd("⌘")  Kbd("K")"#) { HStack(spacing: 6) { Kbd("⌘"); Kbd("K"); Text("then").font(.caption).foregroundStyle(.secondary); Kbd("esc") } },
        .knob("Status", .atoms, demo: StatusDotDemo(), usage: #"StatusDot(.online, label: "Online").pulse()"#),
        .knob("Swap", .atoms, demo: SwapDemo(), usage: #"Swap(isOn: $on).symbols(on: "xmark", off: "line.3.horizontal")"#),
        .knob("TextLink", .atoms, demo: TextLinkDemo(), usage: #"TextLink("Forgot password?") { }"#),
        .knob("Rating", .atoms, demo: RatingDemo(), usage: #"Rating(value: 4.5).allowHalf().onRate { value = $0 }"#),
        .knob("ScoreBadge", .atoms, demo: ScoreBadgeDemo(), usage: #"ScoreBadge(9.0, large: false)"#),
        .knob("Skeleton", .atoms, demo: SkeletonDemo(), usage: #"Text("Loading…").skeleton(isLoading)"#),
        .knob("Spinner", .atoms, demo: SpinnerDemo(), usage: #"Spinner().size(24).lineWidth(3)"#),
        .knob("Tag", .atoms, demo: TagDemo(), usage: #"Tag("Sold out", onRemove: { }).tagStyle(.error).variant(.solid)"#),
        .knob("Title", .atoms, demo: TitleDemo(), usage: #"Title("Section").subtitle("Sub").action("See all", action: { })"#),
        .static("InlineText", .atoms, usage: #"InlineText("Accept the Terms.", links: [("Terms", { })])"#) {
            InlineText("By continuing you accept the Terms and the Privacy Policy.", links: [("Terms", {}), ("Privacy Policy", {})])
        },
        .static("Join", .atoms, usage: #"Join { ButtonA; ButtonB; ButtonC }   // connected group, rounded outer corners"#) {
            Join { ForEach(["Day", "Week", "Month"], id: \.self) { Text($0).textStyle(.labelBase600).padding(.horizontal, 14).frame(height: 40) } }
        },
        .static("Mask", .atoms, usage: #"image.themeMask(.squircle)   // .circle / .squircle / .hexagon / .star"#) {
            HStack(spacing: 14) { ForEach(MaskShape.allCases, id: \.self) { Rectangle().fill(.blue.gradient).frame(width: 52, height: 52).themeMask($0) } }
        },
        .static("TextRotate", .atoms, usage: #"TextRotate(["faster.", "themed.", "accessible."], interval: 2)"#) {
            HStack(spacing: 4) { Text("Build").textStyle(.headingSm); TextRotate(["faster.", "themed.", "accessible."]) }
        },
        .static("Gauge", .atoms, usage: #"GaugeView(value: 0.72, label: "CPU").gaugeStyle(.circular)"#) {
            HStack(spacing: 24) { GaugeView(value: 0.72, label: "CPU"); GaugeView(value: 0.4, label: "Disk").gaugeStyle(.linear).frame(width: 140) }
        },
        .static("ShareButton", .atoms, usage: #"ShareButton(item: url)   // wraps SwiftUI ShareLink"#) {
            ShareButton(item: "https://github.com/isamercan/ThemeKit")
        },
        .static("PriceTag", .atoms, usage: #"PriceTag(1_299).original(1_899).unit("/ night").size(.large).emphasis(.hero).discountBadge()"#) {
            VStack(alignment: .leading, spacing: 12) {
                PriceTag(1_299).original(1_899).unit("/ night").size(.large).emphasis(.hero).discountBadge()
                PriceTag(2_499, currencyCode: "EUR").emphasis(.hero)
                PriceTag(1_299).size(.small)
            }
        },
        .static("PointsBadge", .atoms, usage: #"PointsBadge(1_250).unit("mil").style(.earn).size(.large)"#) {
            VStack(alignment: .leading, spacing: 12) {
                PointsBadge(1_250).unit("mil").style(.earn).size(.large)
                PointsBadge(500).style(.redeem)
                PointsBadge(8_430).style(.balance).icon("wallet.pass.fill")
            }
        },
        .static("CountdownTimer", .atoms, usage: #"CountdownTimer(until: deadline).style(.urgent).size(.large)"#) {
            CountdownTimer(until: .now.addingTimeInterval(9 * 60 + 58)).style(.urgent).size(.large)
        },
        .static("QRCode", .atoms, usage: #"QRCode("https://themekit.dev/pass/BID12025").size(160)   // CoreImage, no dep"#) {
            QRCode("https://github.com/isamercan/ThemeKit").size(160)
        },
        .static("Barcode", .atoms, usage: #"Barcode("9824097217421298").height(56).showsValue()   // Code 128, no dep"#) {
            Barcode("9824097217421298").height(56).showsValue().frame(maxWidth: 300)
        },

        // MARK: Molecules
        .static("ColorField", .molecules, usage: #"ColorField("Brand color", selection: $color)"#) {
            ColorField("Brand color", selection: .constant(.blue)).frame(maxWidth: 320)
        },
        .knob("Autocomplete", .molecules, demo: AutocompleteDemo(), usage: #"Autocomplete("Destination", text: $text, suggestions: items)\n// async: Autocomplete(text: $text, suggest: { await api.search($0) })"#),
        .knob("Button", .molecules, demo: ButtonDemo(), usage: #"PrimaryButton("Continue") { }"#),
        .knob("ButtonGroup", .molecules, demo: ButtonGroupDemo(), usage: #"ButtonGroup { PrimaryButton("OK") { } }"#),
        .knob("ThemeButton", .molecules, demo: ThemeButtonDemo(), usage: #"ThemeButton("Save") { }.color(.success).variant(.soft).size(.medium).shape(.pill)"#),
        .knob("Checkbox", .molecules, demo: CheckboxDemo(), usage: #"Checkbox("Accept terms", isChecked: $on).infoMessages(error ? [.init("Required", kind: .error)] : [])"#),
        .knob("CheckboxGroup", .molecules, demo: CheckboxGroupDemo(), usage: #"CheckboxGroup(options: items, selection: $set) { $0 }"#),
        .knob("InputNumber", .molecules, demo: InputNumberDemo(), usage: #"InputNumber("Max price", value: $n, range: 0...10000).step(50).unit("$")"#),
        .knob("MultiLineTextInput", .molecules, demo: MultiLineDemo(), usage: #"MultiLineTextInput("Notes", text: $text).characterLimit(200)"#),
        .knob("OTPInput", .molecules, demo: OTPDemo(), usage: #"OTPInput(code: $code) { verify($0) }\n         .digitCount(6).secure().resend(interval: 30) { resend() }"#),
        .knob("Breadcrumbs", .molecules, demo: BreadcrumbsDemo(), usage: #"Breadcrumbs([.init("Home", action: { }), .init("Current")], maxItems: 4)"#),
        .knob("Calendar", .molecules, demo: CalendarDemo(), usage: #"CalendarView(selection: $date)"#),
        .knob("DateField", .molecules, demo: DateFieldDemo(), usage: ##"DateField("Check-in", date: $date).style(.custom("EEE, d MMM")).clearable()"##),
        .knob("Fieldset", .molecules, demo: FieldsetDemo(), usage: #"Fieldset("Contact") { inputs }.helper("…")"#),
        .knob("Form", .molecules, demo: FormDemo(), usage: ##"@State var form = FormValidator<Field>([.email: [.required(), .email()]])\nform.validateAll([.email: email])  // → first invalid field, focuses it"##),
        .knob("FileInput", .molecules, demo: FileInputDemo(), usage: #"FileInput("Passport") { pick() }.fileName(name)"#),
        .knob("FilterGroup", .molecules, demo: FilterGroupDemo(), usage: #"FilterGroup(options: items, selection: $sel) { $0 }"#),
        .knob("Chips", .molecules, demo: ChipsDemo(), usage: #"CompactChip("Suit", price: "$899", isSelected: $on).rating(4.6)   // ChoseChip · ImageChip · FilterChip · ChipGroup"#),
        .knob("ProgressIndicator", .molecules, demo: ProgressIndicatorDemo(), usage: #"ProgressIndicator(variant: .carousel, current: 2, total: 8).stepText(.slash)"#),
        .knob("ThemeController", .molecules, demo: ThemeControllerDemo(), usage: #"ThemeController(options: [.init(name: "oceanTheme", label: "Ocean")], selectedName: $name)"#),
        .knob("Pagination", .molecules, demo: PaginationDemo(), usage: #"Pagination(current: $page, total: 50).window(sibling: 2).jumper()"#),
        .knob("Stat", .molecules, demo: StatDemo(), usage: #"Stat(title: "Bookings", value: "1,284").icon("ticket").trend(.up("+12%"))"#),
        .knob("Steps", .molecules, demo: StepsDemo(), usage: #"Steps([.init("Cart", description: "2 items", state: .done), .init("Pay", state: .error)]) { active = $0 }"#),
        .knob("QuantityStepper", .molecules, demo: QuantityStepperDemo(), usage: #"QuantityStepper(value: $qty, range: 0...10)"#),
        .knob("Micro-motion", .molecules, demo: MicroMotionDemo(), usage: #"View().microAnimations(false)   // theme-wide at root, or per-component"#),
        .knob("RadioButton", .molecules, demo: RadioButtonDemo(), usage: #"RadioButton(isSelected: $on)"#),
        .knob("RadioGroup", .molecules, demo: RadioGroupDemo(), usage: #"RadioGroup(options: items, selection: $sel) { $0 }"#),
        .knob("RangeSlider", .molecules, demo: RangeSliderDemo(), usage: #"RangeSlider(lowerValue: $lo, upperValue: $hi, in: 0...1000).step(50).marks([0, 500, 1000]).onChangeEnd(search)"#),
        .knob("SearchBar", .molecules, demo: SearchBarDemo(), usage: #"SearchBar(text: $text).suggestions(cities).recent(recent).onCommit(search)"#),
        .knob("SegmentedControl", .molecules, demo: SegmentedControlDemo(), usage: #"SegmentedControl([SegmentItem("List", systemImage: "list.bullet")], selection: $i)"#),
        .knob("Select", .molecules, demo: SelectDemo(), usage: #"Select("City", options: items, selection: $city) { $0 }.searchable().loading(loading)"#),
        .knob("MultiSelect", .molecules, demo: MultiSelectDemo(), usage: #"MultiSelect("Cities", options: items, selection: $set) { $0 }.optionEnabled { $0.inStock }.loading(loading)"#),
        .knob("TreeSelect", .molecules, demo: TreeSelectDemo(), usage: #"TreeSelect("Cities", nodes: tree, selection: $set, initiallyExpanded: ["tr"]).cascade().searchable()"#),
        .knob("SelectBox", .molecules, demo: SelectBoxDemo(), usage: #"SelectBox("Country", options: items, selection: $sel) { $0 }"#),
        .knob("Slider", .molecules, demo: SliderDemo(), usage: #"Slider(value: $v, in: 0...8).marks([0: "0", 8: "Max"]).showsValueTooltip()"#),
        .knob("TextInput", .molecules, demo: TextInputDemo(), usage: ##"TextInput("Email", text: $t).keyboard(.emailAddress, contentType: .emailAddress, submit: .next)"##),
        .knob("ThemeToggle", .molecules, demo: ToggleDemo(), usage: #"ThemeToggle(isOn: $on).symbols(on: "checkmark")"#),
        .knob("ToggleGroup", .molecules, demo: ToggleGroupDemo(), usage: #"ToggleGroup(options: items, selection: $set, label: { $0 })"#),
        .knob("Tooltip", .molecules, demo: TooltipDemo(), usage: #"anchorView.tooltip("Hint", isPresented: $shown, edge: .top)"#),
        .static("GuestSelector", .molecules, usage: #"GuestSelector(selection: $guests).showsRooms(false)"#) {
            StatefulPreview(GuestSelection(rooms: 1, adults: 2, children: 1)) { guests in
                VStack(alignment: .leading, spacing: 12) {
                    Text(guests.wrappedValue.summary).textStyle(.bodyBase400)
                    GuestSelector(selection: guests)
                }.frame(maxWidth: 340)
            }
        },
        .static("AmenityGrid", .molecules, usage: #"AmenityGrid([Amenity("Free Wi-Fi", systemImage: "wifi"), …]).columns(2)"#) {
            AmenityGrid([
                ThemeKit.Amenity("Free Wi-Fi", systemImage: "wifi"),
                ThemeKit.Amenity("Pool", systemImage: "figure.pool.swim"),
                ThemeKit.Amenity("Breakfast", systemImage: "fork.knife"),
                ThemeKit.Amenity("Parking", systemImage: "parkingsign"),
                ThemeKit.Amenity("Gym", systemImage: "dumbbell"),
                ThemeKit.Amenity("Pet friendly", systemImage: "pawprint"),
            ]).columns(2).frame(maxWidth: 340)
        },
        .static("PriceHistogram", .molecules, usage: #"PriceHistogram(bins: counts, lowerValue: $low, upperValue: $high, in: 0...5_000)"#) {
            PriceHistogramDemo()
        },
        .static("InstallmentSelector", .molecules, usage: #"InstallmentSelector(total: 12_000, options: [1, 3, 6, 12], selection: $months).interestFreeUpTo(3)"#) {
            StatefulPreview(3) { months in
                InstallmentSelector(total: 12_000, options: [1, 3, 6, 12], selection: months).interestFreeUpTo(3).frame(maxWidth: 340)
            }
        },
        .static("CurrencyPicker", .molecules, usage: #"CurrencyPicker(selection: $code, currencies: Currency.common)"#) {
            StatefulPreview("TRY") { code in
                CurrencyPicker(selection: code, currencies: ThemeKit.Currency.common).frame(maxWidth: 340)
            }
        },

        // MARK: Organisms
        .knob("Accordion", .organisms, demo: AccordionDemo(), usage: #"Accordion("Title", initiallyExpanded: false) { Text("Body") }"#),
        .knob("AccordionGroup", .organisms, demo: AccordionGroupDemo(), usage: #"AccordionGroup(faqs) { $0.q } content: { Text($0.a) }.mode(.single)"#),
        .knob("AlertToast", .organisms, demo: AlertToastDemo(), usage: #"AlertToast("Saved").variant(.success).onClose { }"#),
        .knob("BlogCard", .organisms, demo: BlogCardDemo(), usage: #"BlogCard(title: "…") { mediaView }.excerpt("…").readMore { }"#),
        .knob("BottomSheet", .organisms, demo: BottomSheetDemo(), usage: #"// install once: .sheetHost()\n@Environment(SheetPresenter.self) var sheet: SheetPresenter\nsheet.present(detents: [.height(280), .large]) { FilterView() }\n// or declarative: someView.bottomSheet(isPresented: $open, detents: [.medium]) { … }"#),
        .knob("Callout", .organisms, demo: CalloutDemo(), usage: #"Callout("Message").variant(.success).calloutStyle(.plain)"#),
        .knob("Card", .organisms, demo: CardDemo(), usage: #"Card { content }.elevation(.soft)"#),
        .knob("ChatBubble", .organisms, demo: ChatBubbleDemo(), usage: #"ChatBubble("Hi!", time: "09:24").side(.outgoing).icon("person.fill")"#),
        .knob("Counter", .organisms, demo: CounterDemo(), usage: #"Counter(days: 2, hours: 8, minutes: 45)"#),
        .knob("DataTable", .organisms, demo: DataTableDemo(), usage: #"DataTable(columns: cols, rows: rows, selection: $selected).pageSize(10)"#),
        .knob("Drawer", .organisms, demo: DrawerDemo(), usage: #"someView.drawer(isPresented: $open, edge: .leading) { menu }\n// or imperative: install .drawerHost(); @Environment(DrawerPresenter.self) var drawer: DrawerPresenter\ndrawer.present(edge: .leading) { menu }   // drag-to-dismiss built in"#),
        .knob("FAB", .organisms, demo: FABDemo(), usage: #"FloatingActionButton(systemImage: "plus", actions: [.init(systemImage: "camera", action: { })])"#),
        .knob("Hero", .organisms, demo: HeroDemo(), usage: #"Hero(title: "…").subtitle("…").cta("Explore", action: { })"#),
        .knob("Stack", .organisms, demo: CardStackDemo(), usage: #"CardStack(items) { item in cardView }"#),
        .static("Footer", .organisms, usage: #"Footer(columns: [.init("Company", items: [.init("About")])], note: "© 2026")"#) {
            Footer(columns: [.init("Company", items: [.init("About"), .init("Careers")]), .init("Support", items: [.init("Help"), .init("Contact")]), .init("Legal", items: [.init("Terms"), .init("Privacy")])], note: "© 2026 ThemeKit.")
        },
        .static("Diff", .organisms, usage: #"Diff { beforeView } after: { afterView }"#) {
            Diff {
                Theme.shared.background(.bgHero).overlay(Text("BEFORE").foregroundStyle(.white).font(.headline))
            } after: {
                Theme.shared.background(.bgTertiary).overlay(Text("AFTER").foregroundStyle(.white).font(.headline))
            }
        },
        .knob("Timeline", .organisms, demo: TimelineDemo(), usage: #"Timeline([.init(title: "Placed", state: .done, color: .success)]).pending("Awaiting…")"#),
        .knob("Coupon", .organisms, demo: CouponDemo(), usage: #"Coupon(code: "UXMUQ", onCopy: { }).couponStyle(.outlined)"#),
        .knob("EmptyState", .organisms, demo: EmptyStateDemo(), usage: #"EmptyState("Empty").icon("tray").message("…").primaryAction("Retry") { }"#),
        .knob("Feedback", .organisms, demo: FeedbackDemo(), usage: #"@Environment(FeedbackPresenter.self) var feedback: FeedbackPresenter\nfeedback.toast("Saved", kind: .success)              // stacks\nfeedback.toast("Deleted", action: ToastAction("Undo") { }, duration: nil)\nawait feedback.toastTask(loading: "Saving…", success: "Saved") { try await save() }\n// install once: .feedbackHost(maxVisibleToasts: 3, toastPosition: .bottom)"#),
        .knob("Gallery", .organisms, demo: GalleryDemo(), usage: #"Gallery(items) { item in mediaView }.columns(2).aspect(.square)"#),
        .knob("ImageCollage", .organisms, demo: ImageCollageDemo(), usage: #"ImageCollage(urls) { index in open(index) }.height(220)   // 1·2·3·4+ layouts + "+N""#),
        .knob("InfoBanner", .organisms, demo: InfoBannerDemo(), usage: #"InfoBanner("Message", links: [("link", action)]).variant(.info)"#),
        .knob("ListRow", .organisms, demo: ListRowDemo(), usage: #"ListRow("Account", action: { }).subtitle("…").trailing(.chevron)"#),
        .knob("List", .organisms, demo: ListDemo(), usage: #"ListView(items) { ListRow($0.title) }.header("Settings").footer("3 items").bordered()"#),
        .knob("MenuCard", .organisms, demo: MenuCardDemo(), usage: #"MenuCard(items: [.init(title: "Reservations", systemImage: "calendar")])"#),
        .knob("NavigationBar", .organisms, demo: NavigationBarDemo(), usage: #"NavigationBar(items: [.init(systemImage: "house")], selection: $tab)"#),
        .knob("NotificationCard", .organisms, demo: NotificationDemo(), usage: #"NotificationCard(title: "…").message("…").date("…").unread()"#),
        .knob("PageHeader", .organisms, demo: PageHeaderDemo(), usage: #"PageHeader("Title").subtitle("…").onBack { }"#),
        .knob("PromoBanner", .organisms, demo: PromoBannerDemo(), usage: #"PromoBanner("…", action: { }).icon("sun.max.fill").ctaTitle("Go")"#),
        .knob("RatingSummary", .organisms, demo: RatingSummaryDemo(), usage: #"RatingSummary(score: 9.0).label("Excellent").reviews(count: 1200)"#),
        .knob("Result", .organisms, demo: ResultDemo(), usage: #"ResultView(.notFound, title: "Page not found").message("…").primaryAction("Home") { }"#),
        .knob("Popconfirm", .organisms, demo: PopconfirmDemo(), usage: ##"trigger.popconfirm(isPresented: $show, title: "Delete?", confirmTitle: "Delete") { delete() }"##),
        .knob("Tour", .organisms, demo: TourDemo(), usage: ##"view.tourTarget("search");  root.tourHost(tour, steps: [TourStep("search", title: "…", message: "…")])"##),
        .knob("Dialog", .organisms, demo: DialogDemo(), usage: #"view.dialog(isPresented: $show, title: "…") { content } footer: { buttons }"#),
        .knob("SegmentedTabBar", .organisms, demo: SegmentedTabBarDemo(), usage: #"SegmentedTabBar([TabItem("Reviews", badge: "12"), TabItem("Off", isEnabled: false)], selection: $i)"#),
        .knob("SelectionCards", .organisms, demo: SelectionCardsDemo(), usage: #"RadioCard("Standard", isSelected: sel == id) { sel = id }.description("…")"#),
        .knob("Upload", .organisms, demo: UploadDemo(), usage: #"@State var uploads = UploadController()\nUploadList(controller: uploads) { /* pick */ }\nawait uploads.upload(name: file.name) { progress in /* report 0…1 */ }"#),
        .knob("Carousel", .organisms, demo: CarouselDemo(), usage: #"Carousel(items) { item in mediaView }.autoplay(2).arrows()"#),
        .knob("PagingCarousel", .organisms, demo: PagingCarouselDemo(), usage: #"PagingCarousel(items) { item in mediaView }.peek(36).autoplay(2)"#),
        .knob("VideoPlayer", .organisms, demo: VideoPlayerDemo(), usage: #"VideoPlayerView(url).loop().muted().muteToggle()"#),
        .static("KeyValueTable", .organisms, usage: #"KeyValueTable(rows: [...]).title("Summary").bordered()"#) {
            KeyValueTable(rows: [.init("Status", value: "Active", style: .success), .init("Old price", value: "$5,000", style: .strikethrough), .init("Total", value: "$4,250")]).title("Reservation summary").bordered()
        },
        .static("FlightCard", .organisms, usage: #"FlightCard(airline: "Anadolu Air", from: "IST", to: "ESB", departure: dep, arrival: arr).price(1_299).badge("Cheapest") { }"#) {
            VStack(spacing: 12) {
                FlightCard(airline: "Anadolu Air", from: "IST", to: "ESB", departure: .now, arrival: .now.addingTimeInterval(2 * 3_600 + 20 * 60)).price(1_299).badge("Cheapest").onSelect { }
                FlightCard(airline: "Blue Wings", from: "IST", to: "AMS", departure: .now, arrival: .now.addingTimeInterval(4 * 3_600)).stops(1).price(3_499)
            }.frame(maxWidth: 360)
        },
        .static("FareSummary", .organisms, usage: #"FareSummary([.item("Base fare", 1_100), .discount("Member", 100), .total("Total", 1_199)])"#) {
            FareSummary([.item("Base fare", 1_100), .item("Taxes & fees", 199), .discount("Member discount", 100), .total("Total", 1_199)]).frame(maxWidth: 360)
        },
        .static("ReviewCard", .organisms, usage: #"ReviewCard(author: "Elif K.", score: 9.2, text: "…").date(d).verified()"#) {
            ReviewCard(author: "Elif Kaya", score: 9.2, text: "Spotless rooms and a great location right by the marina. Breakfast was excellent.").date(.now).title("Would absolutely stay again").verified().frame(maxWidth: 360)
        },
        .static("LoyaltyCard", .organisms, usage: #"LoyaltyCard(tier: "Gold", points: 8_430).memberName("Elif K.").progress(0.62, toNextTier: "Platinum")"#) {
            LoyaltyCard(tier: "Gold", points: 8_430).memberName("Elif Kaya").progress(0.62, toNextTier: "Platinum").frame(maxWidth: 360)
        },
        .static("SeatMap", .organisms, usage: #"SeatMap(rows: layout, selection: $picked).maxSelection(2)"#) {
            SeatMapDemo()
        },
        .static("LocationCard", .organisms, usage: #"LocationCard(title: "Marina Bay Hotel", latitude: 38.42, longitude: 27.14).subtitle("…").distance("1.2 km")"#) {
            LocationCard(title: "Marina Bay Hotel", latitude: 38.4237, longitude: 27.1428).subtitle("Kordon Cd. No:12, İzmir").distance("1.2 km to center").frame(maxWidth: 340)
        },
        .knob("Theme Injection", .organisms, demo: ThemeInjectionDemo(), usage: #"let ocean = Theme(); ocean.loadTheme(named: "oceanTheme")\nmySubtree.theme(ocean)   // re-skins just this subtree"#),
    ]

    static func entries(in category: ComponentCategory) -> [ComponentEntry] {
        all.filter { $0.category == category }.sorted { $0.name < $1.name }
    }
}
