shadimage
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 · light
ui-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";

More from amplo

Browse all components in this library →