amplo
uigradient-pickersource: ok · hit
Amplo Gradient Picker
Linear / radial / conic gradient editing on Base UI primitives — TypeSwitcher/InterpSwitcher/RadialSizeSelect on Select, ReverseStops and the StopList editor as semantic buttons, and a StopEditor composed from this variant's own Hue/Alpha/FormatSwitcher/ChannelInput. Pulls the color picker plus the shared gradient engine (Bar, Area, Overlay, pads, Presets, CssInput, stop popover shell) as registry dependencies.
npx shadcn add "https://amplo.ale.design/r/gradient-picker.json"free
Visual style
gradient
Interaction
tooltipdropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@base-ui/react
iconslucide-react
Registry dependencies
utilshttps://amplo.ale.design/r/color-picker.jsonhttps://amplo.ale.design/r/gradient-picker-engine.json
Files8 · source cached 10/14/2026, 11:39:19 AM
registry/new-york/ui/fill-picker-base/parts/gradient/field.tsx
"use client";
import * as React from "react";
import { Select } from "@base-ui/react/select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
// Base UI variant of the gradient field shell.
//
// FieldShell / FieldDivider / FieldInput / FieldInputGroup / FieldSuffix are
// plain, variant-agnostic markup — re-exported from the original so both
// pickers share one implementation. Only FieldSelect (built on shadcn
// `<Select>` / Radix in the original) is rebuilt here on `@base-ui/react/select`.
export {
FieldShell,
FieldDivider,
FieldInput,
FieldInputGroup,
FieldSuffix,
type FieldInputProps,
} from "@/registry/new-york/ui/fill-picker/parts/field";
export interface FieldSelectProps {
/**
* `standalone` (default) — bordered chevron-select matching every
* picker dropdown (`FormatSwitcher`, `TypeSwitcher`, `InterpSwitcher`,
* `RadialSizeSelect`).
*
* `inline` — borderless variant that lives inside a `FieldShell` (used
* by `ChannelInput`'s format toggle on the left).
*/
variant?: "standalone" | "inline";
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
disabled?: boolean;
placeholder?: string;
/** Trigger button class — extends the variant's defaults. */
className?: string;
/** Props forwarded to the outer wrapper `<div>`. Use for `data-slot`,
* width overrides, etc. */
wrapperProps?: React.HTMLAttributes<HTMLDivElement> & {
[key: `data-${string}`]: string | undefined;
};
/**
* Class applied to the popup (Base UI `Select.Popup`). Defaults to a
* `font-mono text-xs tracking-wide` block so item rows match the
* trigger font.
*/
contentClassName?: string;
"aria-label"?: string;
/**
* Value → label map for the trigger's collapsed display. Base UI's
* `<Select.Value>` shows the raw value unless the root is told about
* item labels via `items` — pass this whenever an option's display
* label differs from its value (e.g. `"linear"` → `"Linear"`).
*/
items?: Record<string, React.ReactNode>;
/** Pass `<FieldSelectItem>`s as children. */
children?: React.ReactNode;
}
/**
* Single source of truth for every dropdown in the Base UI gradient
* picker. Built on `@base-ui/react/select` so keyboard navigation, focus
* management, and positioning are Base UI's — this file only supplies the
* visual layer, matching the Radix `FieldSelect`'s look pixel-for-pixel.
*
* Mirrors the original `FieldSelect` API exactly (same props, same two
* variants) so gradient parts are drop-in replacements of their Radix
* counterparts.
*
* The forwarded ref points at the trigger button so consumers can
* imperatively focus it, matching the original.
*/
export const FieldSelect = React.forwardRef<
HTMLButtonElement,
FieldSelectProps
>(function FieldSelect(
{
variant = "standalone",
value,
defaultValue,
onValueChange,
disabled,
placeholder,
className,
wrapperProps,
contentClassName,
children,
items,
"aria-label": ariaLabel,
},
ref,
) {
const inline = variant === "inline";
const { className: wrapperClassName, ...wrapperRest } = wrapperProps ?? {};
return (
<div
className={cn(
inline
? "relative inline-flex h-full shrink-0 items-center"
: "relative inline-flex items-center",
wrapperClassName,
)}
{...wrapperRest}
>
<Select.Root
items={items}
value={value}
defaultValue={defaultValue}
onValueChange={(v) => {
if (v != null) onValueChange?.(v);
}}
disabled={disabled}
>
<Select.Trigger
ref={ref}
aria-label={ariaLabel}
className={cn(
"flex items-center justify-between gap-2 font-mono text-xs tracking-wide outline-none",
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
inline
? "h-full rounded-none border-0 bg-transparent px-2 focus-visible:ring-0"
: "h-8 w-full rounded-md border border-input bg-transparent px-3 shadow-xs focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
className,
)}
>
<Select.Value placeholder={placeholder} />
<Select.Icon>
<ChevronDown className="size-4 shrink-0 opacity-50" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner sideOffset={4} className="z-50 outline-none">
<Select.Popup
className={cn(
"min-w-[var(--anchor-width)] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none",
"font-mono text-xs tracking-wide",
contentClassName,
)}
>
{children}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
</div>
);
});
export interface FieldSelectItemProps {
value: string;
className?: string;
children?: React.ReactNode;
}
/**
* Base UI counterpart of shadcn's `<SelectItem>` — the item type
* `<FieldSelect>` expects as children. Kept intentionally minimal (value +
* children only) since every gradient dropdown option is plain text or a
* text row with a trailing info tooltip.
*/
export const FieldSelectItem = React.forwardRef<
HTMLDivElement,
FieldSelectItemProps
>(function FieldSelectItem({ value, className, children }, ref) {
return (
<Select.Item
ref={ref}
value={value}
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none",
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<Select.ItemIndicator>
<Check className="size-4" />
</Select.ItemIndicator>
</span>
<Select.ItemText>{children}</Select.ItemText>
</Select.Item>
);
});
registry/new-york/ui/fill-picker-base/parts/gradient/type-switcher.tsx
"use client";
import * as React from "react";
import { useGradientPickerContext } from "@/registry/new-york/ui/fill-picker/contexts/gradient";
import type { GradientType } from "@/registry/new-york/ui/fill-picker/lib/gradient";
import { FieldSelect, FieldSelectItem } from "./field";
import { GRADIENT_TYPE_OPTIONS } from "@/registry/new-york/ui/fill-picker/lib/gradient-options";
const TYPES = GRADIENT_TYPE_OPTIONS;
const TYPE_ITEMS = Object.fromEntries(
TYPES.map((t) => [t.value, t.label]),
) as Record<GradientType, string>;
/**
* Base UI port of `<GradientPicker.TypeSwitcher>`. Bound to `gradient.type`.
* Built on `<FieldSelect>` (Base UI `Select` under the hood) so it shares
* its border, focus ring, font, and chevron with every other dropdown in
* the Base UI picker.
*
* Width is intrinsic (not `w-full`) so it can sit next to icon buttons
* like `<GradientPickerBase.ReverseStops>` without stretching the row.
*/
export const TypeSwitcher = React.forwardRef<
HTMLButtonElement,
{ className?: string }
>(function TypeSwitcher({ className }, ref) {
const ctx = useGradientPickerContext();
return (
<FieldSelect
ref={ref}
aria-label="Gradient type"
value={ctx.gradient.type}
onValueChange={(v) => ctx.setType(v as GradientType)}
items={TYPE_ITEMS}
wrapperProps={{
"data-slot": "gradient-type-switcher",
className,
}}
>
{TYPES.map((t) => (
<FieldSelectItem key={t.value} value={t.value}>
{t.label}
</FieldSelectItem>
))}
</FieldSelect>
);
});
registry/new-york/ui/fill-picker-base/parts/gradient/interp-switcher.tsx
"use client";
import * as React from "react";
import { Info } from "lucide-react";
import { cn } from "@/lib/utils";
import { Tooltip } from "@base-ui/react/tooltip";
import { useGradientPickerContext } from "@/registry/new-york/ui/fill-picker/contexts/gradient";
import type { GradientInterp } from "@/registry/new-york/ui/fill-picker/lib/gradient";
import { FieldSelect, FieldSelectItem } from "./field";
import { GRADIENT_INTERP_OPTIONS } from "@/registry/new-york/ui/fill-picker/lib/gradient-options";
const OPTIONS = GRADIENT_INTERP_OPTIONS;
const INTERP_ITEMS = Object.fromEntries(
OPTIONS.map((o) => [o.value, o.label]),
) as Record<GradientInterp, string>;
export interface InterpSwitcherProps {
className?: string;
/** Applied to the select trigger. */
triggerClassName?: string;
}
/**
* Base UI port of `<GradientPicker.InterpSwitcher>`. Bound to the active
* gradient's `interp` property. Switching only changes the blending math
* between stops — stop positions and colors stay identical.
*
* Must render inside `<GradientPickerBase.Root>` — throws otherwise.
*/
export const InterpSwitcher = React.forwardRef<
HTMLButtonElement,
InterpSwitcherProps
>(function InterpSwitcher({ className, triggerClassName }, ref) {
const ctx = useGradientPickerContext();
// delay=0: parity with the Radix shells, which inherit the shadcn wrapper default (0). Base UI's own default is 600.
return (
<Tooltip.Provider delay={0}>
<FieldSelect
ref={ref}
aria-label="Interpolation space"
value={ctx.gradient.interp}
onValueChange={(v) => ctx.setInterp(v as GradientInterp)}
items={INTERP_ITEMS}
wrapperProps={{ "data-slot": "gradient-interp-switcher", className: "w-full" }}
className={cn("w-full", triggerClassName, className)}
>
{OPTIONS.map((opt) => (
<RowWithInfo
key={opt.value}
value={opt.value}
label={opt.label}
description={opt.description}
/>
))}
</FieldSelect>
</Tooltip.Provider>
);
});
function RowWithInfo({
value,
label,
description,
}: {
value: string;
label: string;
description: string;
}) {
return (
<FieldSelectItem value={value} className="pr-8">
<span className="flex w-full items-center gap-2">
<span className="flex-1">{label}</span>
<Tooltip.Root>
<Tooltip.Trigger
render={
<span
role="img"
aria-label={`About ${label}`}
className="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground"
>
<Info className="size-3" aria-hidden />
</span>
}
/>
<Tooltip.Portal>
{/* z-50 on the POSITIONER, not just the popup: the positioner is
the element that competes with the Select popup's z-50
positioner — with z-auto it renders behind the open menu. */}
<Tooltip.Positioner
side="right"
align="center"
sideOffset={4}
className="z-50"
>
<Tooltip.Popup
className={cn(
"z-50 max-w-[220px] overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-[11px] normal-case tracking-normal text-background",
)}
>
{description}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</span>
</FieldSelectItem>
);
}
registry/new-york/ui/fill-picker-base/parts/gradient/radial-size-select.tsx
"use client";
import * as React from "react";
import { Info } from "lucide-react";
import { cn } from "@/lib/utils";
import { Tooltip } from "@base-ui/react/tooltip";
import { useGradientPickerContext } from "@/registry/new-york/ui/fill-picker/contexts/gradient";
import type { RadialSizeKeyword } from "@/registry/new-york/ui/fill-picker/lib/gradient";
import { FieldSelect, FieldSelectItem } from "./field";
import { RADIAL_SIZE_OPTIONS } from "@/registry/new-york/ui/fill-picker/lib/gradient-options";
const SIZE_OPTIONS = RADIAL_SIZE_OPTIONS;
export interface RadialSizeSelectProps {
className?: string;
/** Applied to the select trigger. */
triggerClassName?: string;
}
/** Base UI port of `<GradientPicker.RadialSizeSelect>`. */
export const RadialSizeSelect = React.forwardRef<
HTMLButtonElement,
RadialSizeSelectProps
>(function RadialSizeSelect({ className, triggerClassName }, ref) {
const ctx = useGradientPickerContext();
if (ctx.gradient.type !== "radial") return null;
// delay=0: parity with the Radix shells, which inherit the shadcn wrapper default (0). Base UI's own default is 600.
return (
<Tooltip.Provider delay={0}>
<FieldSelect
ref={ref}
aria-label="Radial size"
value={ctx.gradient.size}
onValueChange={(v) => ctx.setRadialSize(v as RadialSizeKeyword)}
wrapperProps={{ "data-slot": "gradient-radial-size-select", className: "w-full" }}
className={cn("w-full", triggerClassName, className)}
>
{SIZE_OPTIONS.map((opt) => (
<RowWithInfo key={opt.value} value={opt.value} description={opt.description} />
))}
</FieldSelect>
</Tooltip.Provider>
);
});
function RowWithInfo({
value,
description,
}: {
value: string;
description: string;
}) {
return (
<FieldSelectItem value={value} className="pr-8">
<span className="flex w-full items-center gap-2">
<span className="flex-1">{value}</span>
<Tooltip.Root>
<Tooltip.Trigger
render={
// Span (not button) so Base UI Select still owns the click
// for row selection — the icon is just a hover affordance.
<span
role="img"
aria-label={`About ${value}`}
className="inline-flex shrink-0 cursor-help text-muted-foreground hover:text-foreground"
>
<Info className="size-3" aria-hidden />
</span>
}
/>
<Tooltip.Portal>
{/* z-50 on the POSITIONER, not just the popup: the positioner is
the element that competes with the Select popup's z-50
positioner — with z-auto it renders behind the open menu. */}
<Tooltip.Positioner
side="right"
align="center"
sideOffset={4}
className="z-50"
>
<Tooltip.Popup
className={cn(
"z-50 max-w-[220px] overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-[11px] normal-case tracking-normal text-background",
)}
>
{description}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</span>
</FieldSelectItem>
);
}
registry/new-york/ui/fill-picker-base/parts/gradient/reverse-stops.tsx
"use client";
// Re-export, not a fork.
//
// This part is a plain shadcn `<Button>` over `ctx.reverseStops` with zero
// Radix-specific surface — since the shared primitives moved to Base UI
// (`useRender`, no Radix `Slot`), the original works unchanged in this
// variant. Forking it bought nothing and risked the two copies drifting.
//
// The file is kept at this path (rather than dropped in favour of importing
// the original directly in `gradient.tsx`) so anyone deep-importing
// `.../fill-picker-base/parts/gradient/reverse-stops` keeps resolving.
export { ReverseStops } from "@/registry/new-york/ui/fill-picker/parts/gradient/reverse-stops";
registry/new-york/ui/fill-picker-base/parts/gradient/stop-list.tsx
"use client";
// Re-export, not a fork.
//
// The original StopList touches no Radix primitive: its rows are plain markup,
// its keyboard/selection/insert logic already lives in the shared
// `stop-list-shared` helpers, its stop editor is a slot injected by whichever
// barrel mounted the tree (`useGradientStopEditor` — so under this variant the
// rows open ./stop-editor, the Base UI one), and the only field parts it uses
// — FieldInput / FieldInputGroup / FieldShell / FieldSuffix — are re-exported
// verbatim from the original by this variant's own `./field`. It never touches
// `FieldSelect`, the one field part that genuinely differs between variants.
//
// So the fork was a byte-for-byte copy modulo import paths, and its only
// effect was the risk of the two lists' keyboard behaviour drifting apart.
//
// The file is kept at this path (rather than dropped in favour of importing
// the original directly in `gradient.tsx`) so anyone deep-importing
// `.../fill-picker-base/parts/gradient/stop-list` keeps resolving.
export {
StopList,
type StopListProps,
} from "@/registry/new-york/ui/fill-picker/parts/gradient/stop-list";
registry/new-york/ui/fill-picker-base/parts/gradient/stop-editor.tsx
"use client";
import * as React from "react";
import { StopPopover } from "@/registry/new-york/ui/fill-picker/parts/gradient/stop-popover";
import { ColorPickerContext } from "@/registry/new-york/ui/fill-picker/context";
import type { GradientStopEditorRenderer } from "@/registry/new-york/ui/fill-picker/contexts/gradient";
import { useStopColorPickerState } from "@/registry/new-york/ui/fill-picker/parts/gradient/stop-editor-shared";
import { Area as ColorArea } from "@/registry/new-york/ui/fill-picker/parts/area";
import { EyeDropper } from "@/registry/new-york/ui/fill-picker/parts/eye-dropper";
import { Hue } from "../hue";
import { Alpha } from "../alpha";
import { ChannelInput } from "../channel-input";
import { FormatSwitcher } from "../format-switcher";
export interface StopEditorProps {
/** Stop the popover edits — color + per-stop format are read/written via gradient context. */
stopId: string;
/**
* Controlled open state. Required — open is always external because
* the anchor element typically has its own pointer handling (drag on
* Bar handles, click on StopList swatches) that would conflict with
* a trigger-managed open state.
*/
open: boolean;
onOpenChange: (open: boolean) => void;
/** Anchor element. Positioned against; provides positioning only. */
children: React.ReactElement;
}
/**
* Base UI variant of `<StopEditorPopover>`: same layout, same bound state
* (`useStopColorPickerState`, shared with the original so the memoization
* subtleties can't drift), but composed from this tree's Base UI parts —
* Hue/Alpha on Slider, FormatSwitcher on Select, ChannelInput on NumberField.
*
* Area and EyeDropper come from the engine unchanged: they're pointer/canvas
* logic and a plain shadcn Button, already shipped as-is by this variant's
* `color-picker.tsx`.
*
* The popover shell is the engine's `StopPopover` — already built directly on
* `@base-ui/react/popover` (anchor-without-trigger, z-50 positioner), so
* there's nothing Radix-flavoured to re-do.
*/
export function StopEditor({
stopId,
open,
onOpenChange,
children,
}: StopEditorProps) {
const state = useStopColorPickerState(stopId);
return (
<StopPopover
open={open}
onOpenChange={onOpenChange}
anchor={children}
className="flex w-72 flex-col gap-3"
onContentClick={(e) => e.stopPropagation()}
>
<ColorPickerContext.Provider value={state}>
<ColorArea mode="oklch-cl" />
<div className="flex flex-col gap-1.5">
<Hue />
<Alpha />
</div>
<div className="flex items-center gap-2">
<FormatSwitcher className="flex-1" />
<EyeDropper className="h-8 w-full flex-1" />
</div>
<ChannelInput showFormat={false} />
</ColorPickerContext.Provider>
</StopPopover>
);
}
/**
* `<GradientPickerBase.Bar editOnClick>`'s stop-editor slot, filled with the
* editor above. Injected as the default by the Base UI barrels
* (`gradient.tsx`, `fill.tsx`).
*/
export const stopEditorSlot: GradientStopEditorRenderer = ({
children,
...props
}) => (
// The Bar always passes a single element (the stop handle) as `children`;
// the slot type is widened to ReactNode only because it is React's own
// children convention.
<StopEditor {...props}>{children as React.ReactElement}</StopEditor>
);
registry/new-york/ui/fill-picker-base/gradient.tsx
"use client";
// Base UI variant of the gradient picker.
//
// Mirrors `registry/new-york/ui/fill-picker/gradient-picker.tsx`'s public
// surface. Only the parts that touch Radix/shadcn primitives directly are
// rewritten on Base UI:
// - TypeSwitcher, InterpSwitcher, RadialSizeSelect → Select
// - ReverseStops → plain button
// - StopList → plain button (+
// the shared field shell), re-exported from the original (see
// stop-list.tsx for why)
// - the per-stop color editor → ./parts/gradient/stop-editor, injected
// below through the `stopEditor` slot. Both `<Bar editOnClick>` and
// `<StopList>` rows read that slot instead of importing an editor, so
// they stay dialect-free and open *this* variant's editor here.
//
// Everything else (Root/context, Bar, Area, Overlay, ShapeSwitcher, the
// pad/input/group parts, StopColor, Presets, CssInput, RepeatingToggle) is
// pure pointer/canvas logic or plain markup with zero Radix dependency, so
// it's imported straight from the original — a single source of truth for
// engine fixes.
import * as React from "react";
import { ColorPickerBase } from "./color-picker";
import {
Root as EngineGradientRoot,
type RootProps as GradientRootProps,
} from "@/registry/new-york/ui/fill-picker/parts/gradient/root";
import { Bar } from "@/registry/new-york/ui/fill-picker/parts/gradient/bar";
import { Area as GradientArea } from "@/registry/new-york/ui/fill-picker/parts/gradient/area";
import { Overlay as GradientOverlay } from "@/registry/new-york/ui/fill-picker/parts/gradient/overlay";
import { RepeatingToggle } from "@/registry/new-york/ui/fill-picker/parts/gradient/repeating-toggle";
import { AnglePad } from "@/registry/new-york/ui/fill-picker/parts/gradient/angle-pad";
import { AngleInput } from "@/registry/new-york/ui/fill-picker/parts/gradient/angle-input";
import { PositionPad } from "@/registry/new-york/ui/fill-picker/parts/gradient/position-pad";
import { PositionInput } from "@/registry/new-york/ui/fill-picker/parts/gradient/position-input";
import { ShapeSwitcher } from "@/registry/new-york/ui/fill-picker/parts/gradient/shape-switcher";
import { RadiusInput } from "@/registry/new-york/ui/fill-picker/parts/gradient/radius-input";
import { EllipseRadiiInput } from "@/registry/new-york/ui/fill-picker/parts/gradient/ellipse-radii-input";
import { StopColor } from "@/registry/new-york/ui/fill-picker/parts/gradient/stop-color";
import { Presets, BUILTIN_GRADIENT_PRESETS } from "@/registry/new-york/ui/fill-picker/parts/gradient/presets";
import { CssInput as GradientCssInput } from "@/registry/new-york/ui/fill-picker/parts/gradient/css-input";
import { PositionGroup } from "@/registry/new-york/ui/fill-picker/parts/gradient/position-group";
import { AngleGroup } from "@/registry/new-york/ui/fill-picker/parts/gradient/angle-group";
import { TypeSwitcher } from "./parts/gradient/type-switcher";
import { ReverseStops } from "./parts/gradient/reverse-stops";
import { StopList } from "./parts/gradient/stop-list";
import { InterpSwitcher } from "./parts/gradient/interp-switcher";
import { RadialSizeSelect } from "./parts/gradient/radial-size-select";
import { stopEditorSlot } from "./parts/gradient/stop-editor";
export type {
Gradient,
GradientType,
GradientInterp,
GradientStop,
LinearGradient,
RadialGradient,
RadialSizeKeyword,
ConicGradient,
} from "@/registry/new-york/ui/fill-picker/lib/gradient";
export {
formatGradient,
parseGradient,
DEFAULT_LINEAR,
DEFAULT_RADIAL,
DEFAULT_CONIC,
} from "@/registry/new-york/ui/fill-picker/lib/gradient";
export { BUILTIN_GRADIENT_PRESETS };
export { useGradientPicker } from "@/registry/new-york/ui/fill-picker/hooks/use-gradient-picker";
export type {
UseGradientPickerProps,
GradientPickerState,
} from "@/registry/new-york/ui/fill-picker/hooks/use-gradient-picker";
// The gradient context surface, re-exported so the barrel is a complete
// entry point for anyone composing custom gradient parts (a part needs
// `useGradientPickerContext` to read state, and `GradientStopEditor*` to
// fill the stop-editor slot). This also keeps the barrel self-consistent
// with how the shadcn CLI rewrites installed imports: its import-fixup pass
// resolves an aliased specifier by file BASENAME and prefers a `.tsx`
// candidate over a `.ts` one, so `.../fill-picker/contexts/gradient.ts`
// gets redirected to this file (`fill-picker-base/gradient.tsx`) in a
// consumer project. Export-from specifiers are exempt from that pass, so
// the re-exports below stay pointed at the real module.
//
// Cycle invariant: because of that same redirect, an INSTALLED consumer's
// sibling parts (`./parts/gradient/{type,interp}-switcher`,
// `radial-size-select`, `stop-editor`, and `./fill`) import their context
// from *this barrel* rather than from `contexts/gradient` — and this barrel
// imports those parts back. That module cycle is benign only because every
// redirected binding is referenced inside a component body / hook call at
// render time, never at module-eval time: by the time anything reads
// `useGradientPickerContext`, both halves of the cycle have finished
// evaluating. Keep new module-scope evaluation out of the base gradient
// parts — a top-level `const x = useGradientPickerContext` or a
// `Context.Provider` captured into a module constant would read a
// still-uninitialized binding and blow up in consumer projects while
// staying green in this repo (where TypeScript resolves the honest path
// and there is no cycle at all). The build's basename-collision lint
// guards the export side of this; nothing can guard the eval side but
// this comment.
export {
GradientPickerContext,
GradientStopEditorContext,
useGradientPickerContext,
useGradientStopEditor,
} from "@/registry/new-york/ui/fill-picker/contexts/gradient";
export type { GradientStopEditorRenderer } from "@/registry/new-york/ui/fill-picker/contexts/gradient";
export { ColorPickerBase };
/**
* The engine `Root` plus this variant's stop editor (Base UI Hue/Alpha/
* FormatSwitcher/ChannelInput inside the shared popover shell). The Bar
* imports no editor of its own — it renders whatever is injected here.
*/
const GradientRoot = React.forwardRef<HTMLDivElement, GradientRootProps>(
function Root({ stopEditor = stopEditorSlot, ...props }, ref) {
return <EngineGradientRoot ref={ref} stopEditor={stopEditor} {...props} />;
},
);
export const GradientPickerBase = {
Root: GradientRoot,
Bar,
Area: GradientArea,
Overlay: GradientOverlay,
TypeSwitcher,
ReverseStops,
RepeatingToggle,
AnglePad,
AngleInput,
PositionPad,
PositionInput,
ShapeSwitcher,
RadiusInput,
EllipseRadiiInput,
RadialSizeSelect,
StopList,
StopColor,
InterpSwitcher,
Presets,
CssInput: GradientCssInput,
PositionGroup,
AngleGroup,
};
// Plain-name aliases so consumers (and docs snippets) can swap variants by
// changing only the import path — mirrors `ColorPicker` in color-picker.tsx.
export const GradientPicker = GradientPickerBase;
export { ColorPicker } from "./color-picker";