shadimage
bjork-ui
componentbonuses-incentives-cardsource: ok · hit

Bonuses incentives card

Bonuses incentives card is a production-ready cards component from BJORK UI, packaged for shadcn registries.

npx shadcn add "https://ui.isaiahbjork.com/bonuses-incentives-card.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion

Files3 · source cached 10/9/2026, 4:50:05 AM

components/bjork-ui/cards/bonuses-incentives-card.tsx
"use client";

import type { CSSProperties } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
import { BjorkButton } from "@/components/bjork-ui/primitives/button";

interface BonusesIncentivesCardProps {
  // Content
  bonusText?: string;
  incentivesText?: string;
  bonusesValue?: number;
  incentivesValue?: number;

  // Styling
  borderColor?: string;
  backgroundColor?: string;
  blurColorBlue?: string;
  blurColorGreen?: string;

  // Dots configuration
  outerDotsCount?: number;
  innerDotsCount?: number;

  // Animation controls
  enableAnimations?: boolean;

  // Callbacks
  onMoreDetails?: () => void;
}

const defaultProps: Partial<BonusesIncentivesCardProps> = {
  bonusText: "Bonus and Incentives",
  incentivesText: "Incentives",
  bonusesValue: 1250,
  incentivesValue: 875,
  borderColor: "border-[color:var(--bjork-border-muted)]",
  backgroundColor: "bg-[var(--bjork-surface)]",
  blurColorBlue: "bg-[#ec5c13]/10",
  blurColorGreen: "bg-[#8a7a60]/10",
  outerDotsCount: 48,
  innerDotsCount: 36,
  enableAnimations: true,
};

export function BonusesIncentivesCard(props: BonusesIncentivesCardProps) {
  const {
    bonusesValue,
    incentivesValue,
    borderColor,
    backgroundColor,
    outerDotsCount,
    innerDotsCount,
    enableAnimations,
    onMoreDetails,
  } = { ...defaultProps, ...props };

  const shouldReduceMotion = useReducedMotion();
  const shouldAnimate = enableAnimations && !shouldReduceMotion;

  // Generate circular dots positions
  const generateDots = (count: number, radius: number, centerX: number, centerY: number) => {
    const dots = [];
    for (let i = 0; i < count; i++) {
      const angle = (i / count) * 2 * Math.PI;
      const x = Math.round((centerX + radius * Math.cos(angle)) * 1000) / 1000;
      const y = Math.round((centerY + radius * Math.sin(angle)) * 1000) / 1000;
      dots.push({ x, y, angle, delay: i * 0.02 });
    }
    return dots;
  };

  const outerDots = generateDots(outerDotsCount!, 185, 203, 200);
  const innerDots = generateDots(innerDotsCount!, 155, 203, 200);

  const bonusDelay = (delay: number): CSSProperties => {
    return { "--bonus-delay": `${delay}s` } as CSSProperties;
  };

  const dotStyle = (color: string, delay: number): CSSProperties => {
    return {
      color,
      "--bonus-delay": `${delay}s`,
    } as CSSProperties;
  };


  return (
    <motion.div
      className={cn("w-full max-w-md", shouldAnimate && "bjork-bonus-card-enter")}
      initial={false}
    >
      <motion.div
        className={cn(
          backgroundColor,
          borderColor,
          "overflow-hidden rounded-[20px] border text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-surface)]"
        )}
      >

        {/* Middle Section - Dots */}
        <div className="relative pl-4 pr-8 pb-4 pt-8 overflow-hidden">
          {/* Blur backgrounds */}
          <div className={cn("absolute inset-0 rounded-lg backdrop-blur-[2px]", backgroundColor)} />

          {/* Dots Container */}
          <div className="relative w-[28rem] h-[28rem] mx-auto">
            <svg className="w-full h-full" viewBox="0 0 448 448">
              {/* Outer dots */}
              {outerDots.map((dot, index) => (
                <motion.circle
                  key={`outer-${index}`}
                  cx={dot.x}
                  cy={dot.y}
                  r="10"
                  data-bonus-dot
                  fill="currentColor"
                  style={dotStyle("#ec5c13", dot.delay)}
                />
              ))}

              {/* Inner dots */}
              {innerDots.map((dot, index) => (
                <motion.circle
                  key={`inner-${index}`}
                  cx={dot.x}
                  cy={dot.y}
                  r="10"
                  data-bonus-dot
                  fill="currentColor"
                  style={dotStyle("#8a7a60", dot.delay + 0.08)}
                />
              ))}
            </svg>

            {/* Center Text Overlay */}
            <div className="absolute inset-0 flex items-center justify-center pointer-events-none -mt-24 -ml-12">
              <div className="text-center" style={{ zIndex: 20 }}>
                <motion.div 
                  data-bonus-enter
                  className="mb-2 text-xl font-medium text-[color:var(--bjork-text-muted)]"
                  initial={false}
                  style={bonusDelay(0.3)}
                >
                  TOTAL
                </motion.div>
                <motion.div 
                  data-bonus-enter
                  className="text-5xl font-bold text-[color:var(--bjork-text)]"
                  initial={false}
                  style={bonusDelay(0.5)}
                >
                  ${(bonusesValue! + incentivesValue!).toLocaleString()}
                </motion.div>
              </div>
            </div>
          </div>

          {/* Gradient fade overlay for bottom half - covers entire card */}
          <div
            className="pointer-events-none absolute -inset-4 rounded-xl"
            style={{
              background:
                "linear-gradient(to bottom, rgb(from var(--bjork-surface) r g b / 0) 0%, rgb(from var(--bjork-surface) r g b / 0) 48%, rgb(from var(--bjork-surface) r g b / 0.54) 58%, rgb(from var(--bjork-surface) r g b / 0.9) 70%, var(--bjork-surface) 86%)",
              zIndex: 5,
            }}
          />

          {/* Bottom Section */}
          <div className="absolute bottom-0 left-0 right-0 px-6 pb-2 pt-4" style={{ zIndex: 10 }}>
          <div className="flex items-start justify-between mb-4">
            {/* Bonuses Section */}
            <div className="flex flex-col items-center gap-2">
              <div className="flex items-center gap-2">
                <motion.div
                  data-bonus-enter
                  className="w-0.5 h-4 rounded-full"
                  initial={false}
                  style={{ backgroundColor: "#ec5c13", ...bonusDelay(0.4) }}
                />
                <motion.div
                  data-bonus-enter
                  className="text-sm font-medium text-[color:var(--bjork-text-muted)]"
                  initial={false}
                  style={bonusDelay(0.5)}
                >
                  Bonuses
                </motion.div>
              </div>
              <div className="flex flex-col">
                <motion.div
                  data-bonus-enter
                  className="text-left text-xl font-bold text-[color:var(--bjork-text)]"
                  initial={false}
                  style={bonusDelay(0.6)}
                >
                  ${bonusesValue!.toLocaleString()}
                </motion.div>
                <motion.div
                  data-bonus-enter
                  className="text-xs font-medium text-left"
                  initial={false}
                  style={{ color: "#ec5c13", ...bonusDelay(0.7) }}
                >
                  +15.2%
                </motion.div>
              </div>
            </div>

            {/* Incentives Section */}
            <div className="flex flex-col items-center gap-2 mb-2">
              <div className="flex items-center gap-2">
                <motion.div
                  data-bonus-enter
                  className="w-0.5 h-4 rounded-full"
                  initial={false}
                  style={{ backgroundColor: "#8a7a60", ...bonusDelay(0.8) }}
                />
                <motion.div
                  data-bonus-enter
                  className="text-sm font-medium text-[color:var(--bjork-text-muted)]"
                  initial={false}
                  style={bonusDelay(0.9)}
                >
                  Incentives
                </motion.div>
              </div>
              <div className="flex flex-col">
                <motion.div
                  data-bonus-enter
                  className="text-left text-xl font-bold text-[color:var(--bjork-text)]"
                  initial={false}
                  style={bonusDelay(1)}
                >
                  ${incentivesValue!.toLocaleString()}
                </motion.div>
                <motion.div
                  data-bonus-enter
                  className="text-xs font-medium text-left"
                  initial={false}
                  style={{ color: "#ec5c13", ...bonusDelay(1.1) }}
                >
                  +8.7%
                </motion.div>
              </div>
            </div>
          </div>

          <BjorkButton
            variant="accent"
            data-bonus-enter
            className="mb-4 w-full"
            initial={false}
            style={bonusDelay(1.1)}
            onClick={onMoreDetails}
          >
            More Details
          </BjorkButton>
        </div>
        </div>

      </motion.div>
    </motion.div>
  );
}
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
components/bjork-ui/primitives/button.tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion, type HTMLMotionProps } from "framer-motion";
import { cva, type VariantProps } from "class-variance-authority";
import { useWebHaptics } from "web-haptics/react";
import type { HapticInput, TriggerOptions } from "web-haptics";
import { cn } from "@/lib/utils";

const bjorkButtonVariants = cva(
  "inline-flex shrink-0 transform-gpu items-center justify-center gap-2 whitespace-nowrap rounded-[13px] text-sm font-medium tracking-[-0.01em] outline-none backdrop-blur-md transition-[background-color,border-color,color,box-shadow,opacity] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent)]/45 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)] disabled:pointer-events-none disabled:opacity-35 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
  {
    variants: {
      variant: {
        default:
          "border border-transparent bg-[var(--bjork-field)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-surface)] hover:bg-[var(--bjork-surface-hover)]",
        secondary:
          "border border-[color:var(--bjork-border)] bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)] hover:bg-[var(--bjork-surface-hover)]",
        outline:
          "border border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] text-[color:var(--bjork-text-strong)] shadow-[var(--bjork-shadow-soft)] hover:border-[color:var(--bjork-border-strong)] hover:bg-[var(--bjork-surface)] hover:text-[color:var(--bjork-text)]",
        ghost:
          "border border-transparent bg-transparent text-[color:var(--bjork-text-muted)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)]",
        quiet:
          "border border-transparent bg-[var(--bjork-field-muted)] text-[color:var(--bjork-text-soft)] hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text-medium)]",
        accent:
          "bjork-layered-button-accent tracking-tight text-white",
        raised:
          "bjork-layered-button-raised tracking-tight",
      },
      size: {
        sm: "h-8 px-3 text-xs has-[>svg:first-child]:pl-2.5 has-[>svg:last-child]:pr-2.5",
        md: "h-[45px] px-4 has-[>svg:first-child]:pl-3.5 has-[>svg:last-child]:pr-3.5",
        lg: "h-12 px-5 text-[15px] has-[>svg:first-child]:pl-4.5 has-[>svg:last-child]:pr-4.5",
        icon: "size-[45px] p-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  },
);

export interface BjorkButtonProps
  extends Omit<HTMLMotionProps<"button">, "ref" | "size">,
    VariantProps<typeof bjorkButtonVariants> {
  haptics?: boolean;
  hapticPattern?: HapticInput;
  hapticOptions?: TriggerOptions;
}

export function BjorkButton({
  className,
  variant,
  size,
  disabled,
  haptics = true,
  hapticPattern = "light",
  hapticOptions,
  onPointerDown,
  tabIndex,
  ...props
}: BjorkButtonProps) {
  const shouldReduceMotion = useReducedMotion();
  const { trigger } = useWebHaptics();

  const handlePointerDown = React.useCallback(
    (event: React.PointerEvent<HTMLButtonElement>) => {
      if (!disabled && haptics) {
        void trigger(hapticPattern, hapticOptions);
      }

      onPointerDown?.(event);
    },
    [disabled, hapticOptions, hapticPattern, haptics, onPointerDown, trigger],
  );

  return (
    <motion.button
      data-slot="bjork-button"
      className={cn(bjorkButtonVariants({ variant, size, className }))}
      disabled={disabled}
      onPointerDown={handlePointerDown}
      whileHover={disabled || shouldReduceMotion ? undefined : { scale: 1.05 }}
      whileTap={disabled || shouldReduceMotion ? undefined : { scale: 0.95 }}
      transition={{ type: "spring", stiffness: 400, damping: 25 }}
      tabIndex={tabIndex ?? 0}
      {...props}
    />
  );
}

export { bjorkButtonVariants };

More from bjork-ui

Browse all components in this library →