{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "turntable",
  "title": "Turntable",
  "description": "A product card spins on an invisible turntable through front, edge and back in real 3D, with face shading and a breathing floor shadow.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/turntable/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type TurntableColors = {\n  background: string;\n  front: string;\n  back: string;\n  edge: string;\n  mark: string;\n  shadow: string;\n};\n\nexport type TurntableProps = {\n  /** Front face content. Defaults to a red card with a mark. */\n  children?: React.ReactNode;\n  /** Back face content. Defaults to a dark card. */\n  back?: React.ReactNode;\n  /** Card thickness in px at 720p. */\n  thickness?: number;\n  /** Full rotations per spin. */\n  turns?: number;\n  /** Camera tilt in degrees; positive looks down on the card. */\n  tilt?: number;\n  colors?: Partial<TurntableColors>;\n  /** Default front face color. Overrides `colors.front`. */\n  frontColor?: string;\n  /** Card edge color. Overrides `colors.edge`. */\n  edgeColor?: string;\n  /** Default back face color. Overrides `colors.back`. */\n  backColor?: string;\n  /** Stage background. Overrides `colors.background`. */\n  backgroundColor?: string;\n  speed?: number;\n};\n\nexport const turntableDefaults = {\n  thickness: 22,\n  turns: 1,\n  tilt: 8,\n  colors: {\n    background: \"#111111\",\n    front: \"#E5322D\",\n    back: \"#F2F2F2\",\n    edge: \"#A3201C\",\n    mark: \"#FFFFFF\",\n    shadow: \"#000000\",\n  } satisfies TurntableColors,\n  speed: 1,\n};\n\nconst START = 6;\nconst END = 64;\n\nfunction DefaultFront({ colors }: { colors: TurntableColors }) {\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: colors.front,\n        padding: \"9%\",\n        justifyContent: \"space-between\",\n      }}\n    >\n      <div\n        style={{\n          width: \"22%\",\n          aspectRatio: \"1\",\n          borderRadius: \"50%\",\n          border: `0.6em solid ${colors.mark}`,\n        }}\n      />\n      <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.5em\" }}>\n        <div\n          style={{\n            height: \"0.9em\",\n            width: \"62%\",\n            background: colors.mark,\n            borderRadius: 99,\n          }}\n        />\n        <div\n          style={{\n            height: \"0.9em\",\n            width: \"38%\",\n            background: colors.mark,\n            opacity: 0.55,\n            borderRadius: 99,\n          }}\n        />\n      </div>\n    </AbsoluteFill>\n  );\n}\n\nfunction DefaultBack({ colors }: { colors: TurntableColors }) {\n  return (\n    <AbsoluteFill style={{ backgroundColor: colors.back }}>\n      <div\n        style={{\n          position: \"absolute\",\n          top: \"16%\",\n          left: 0,\n          right: 0,\n          height: \"18%\",\n          background: \"#111111\",\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          right: \"9%\",\n          bottom: \"12%\",\n          width: \"16%\",\n          aspectRatio: \"1\",\n          borderRadius: \"28%\",\n          background: colors.front,\n        }}\n      />\n    </AbsoluteFill>\n  );\n}\n\nexport function Turntable({\n  children,\n  back,\n  thickness = turntableDefaults.thickness,\n  turns = turntableDefaults.turns,\n  tilt = turntableDefaults.tilt,\n  colors: colorOverrides,\n  frontColor,\n  edgeColor,\n  backColor,\n  backgroundColor,\n  speed = turntableDefaults.speed,\n}: TurntableProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const baseColors = { ...turntableDefaults.colors, ...colorOverrides };\n  const colors = {\n    ...baseColors,\n    front: frontColor ?? baseColors.front,\n    edge: edgeColor ?? baseColors.edge,\n    back: backColor ?? baseColors.back,\n    background: backgroundColor ?? baseColors.background,\n  };\n  const t = (frame * speed * 30) / fps;\n  const unit = height / 720;\n\n  const cardH = height * 0.46;\n  const cardW = cardH * 1.58;\n  const depth = thickness * unit;\n  const radius = Math.min(cardH * 0.06, depth * 1.2);\n\n  const spin = interpolate(t, [START, END], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.55, 0, 0.25, 1),\n  });\n  const angle = spin * 360 * turns;\n  const rad = (angle * Math.PI) / 180;\n  const cos = Math.cos(rad);\n  const sin = Math.sin(rad);\n\n  // Lambert-ish shading for each face from a key light front-left.\n  const light = (nx: number, nz: number) =>\n    0.55 + 0.45 * Math.max(0, nz * 0.85 - nx * 0.5);\n  const frontShade = light(sin, cos);\n  const backShade = light(-sin, -cos);\n  const sideShade = light(cos, -sin);\n  const sheen = 50 + sin * 120;\n\n  const footprint = Math.abs(cos) * cardW + Math.abs(sin) * depth;\n  const shadowW = Math.max(footprint, depth * 3);\n  const floorY = height / 2 + cardH / 2 + height * 0.075;\n\n  const face = (\n    content: React.ReactNode,\n    transform: string,\n    shade: number,\n  ): React.ReactNode => (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        borderRadius: radius,\n        overflow: \"hidden\",\n        backfaceVisibility: \"hidden\",\n        transform,\n        fontSize: cardH / 30,\n      }}\n    >\n      {content}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `linear-gradient(105deg, rgba(255,255,255,0) ${sheen - 22}%, rgba(255,255,255,0.22) ${sheen}%, rgba(255,255,255,0) ${sheen + 22}%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: \"#000\",\n          opacity: 1 - shade,\n        }}\n      />\n    </div>\n  );\n\n  const slab = (\n    w: number,\n    h: number,\n    transform: string,\n    shade: number,\n    key: string,\n  ) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left: (cardW - w) / 2,\n        top: (cardH - h) / 2,\n        width: w,\n        height: h,\n        background: colors.edge,\n        filter: `brightness(${shade})`,\n        transform,\n      }}\n    />\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: colors.background,\n        overflow: \"hidden\",\n        background: `radial-gradient(ellipse 60% 40% at 50% ${(floorY / height) * 100}%, rgba(255,255,255,0.07), rgba(255,255,255,0) 70%), ${colors.background}`,\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          left: width / 2 - shadowW / 2,\n          top: floorY - height * 0.025,\n          width: shadowW,\n          height: height * 0.05,\n          borderRadius: \"50%\",\n          background: colors.shadow,\n          opacity: 0.45 + 0.35 * Math.abs(cos),\n          filter: `blur(${height * 0.018}px)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          perspective: height * 2.4,\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div\n          style={{\n            width: cardW,\n            height: cardH,\n            position: \"relative\",\n            transformStyle: \"preserve-3d\",\n            transform: `rotateX(${-tilt}deg) rotateY(${angle}deg)`,\n          }}\n        >\n          {slab(\n            depth,\n            cardH - radius * 2,\n            `rotateY(90deg) translateZ(${cardW / 2}px)`,\n            sideShade,\n            \"r\",\n          )}\n          {slab(\n            depth,\n            cardH - radius * 2,\n            `rotateY(-90deg) translateZ(${cardW / 2}px)`,\n            light(-cos, sin),\n            \"l\",\n          )}\n          {slab(\n            cardW - radius * 2,\n            depth,\n            `rotateX(90deg) translateZ(${cardH / 2}px)`,\n            1.1,\n            \"t\",\n          )}\n          {slab(\n            cardW - radius * 2,\n            depth,\n            `rotateX(-90deg) translateZ(${cardH / 2}px)`,\n            0.6,\n            \"b\",\n          )}\n          {face(\n            children ?? <DefaultFront colors={colors} />,\n            `translateZ(${depth / 2}px)`,\n            frontShade,\n          )}\n          {face(\n            back ?? <DefaultBack colors={colors} />,\n            `rotateY(180deg) translateZ(${depth / 2}px)`,\n            backShade,\n          )}\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/turntable.tsx"
    }
  ],
  "type": "registry:component"
}
